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
data/lib/parsers/xml.dtd
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
2
|
+
|
3
|
+
<!--
|
4
|
+
Document type definition of XML document defining dynamic images
|
5
|
+
PUBLIC ID: -//malis//dynamic_images//EN
|
6
|
+
SYSTEM ID: https://raw.github.com/malis/dynamic_images/master/lib/parsers/xml.dtd
|
7
|
+
|
8
|
+
-->
|
9
|
+
|
10
|
+
<!--
|
11
|
+
An example how to use this DTD from your XML document:
|
12
|
+
|
13
|
+
<?xml version="1.0"?>
|
14
|
+
|
15
|
+
<!DOCTYPE dynamic_images PUBLIC "-//malis//dynamic_images//EN" "https://raw.github.com/malis/dynamic_images/master/lib/parsers/xml.dtd">
|
16
|
+
|
17
|
+
<dynamic_images>
|
18
|
+
...
|
19
|
+
</dynamic_images>
|
20
|
+
-->
|
21
|
+
|
22
|
+
<!--- Root element dynamic_images can contain also more than one dynamic_image definition -->
|
23
|
+
<!ELEMENT dynamic_images (dynamic_image)*>
|
24
|
+
|
25
|
+
<!--- Block element and inherited classes which can contain same elements -->
|
26
|
+
<!ENTITY % BlockElements "(block|image|table|text)*">
|
27
|
+
<!ELEMENT dynamic_image %BlockElements;>
|
28
|
+
<!ELEMENT block %BlockElements;>
|
29
|
+
<!ENTITY % common_attrs
|
30
|
+
"height CDATA #IMPLIED
|
31
|
+
h CDATA #IMPLIED
|
32
|
+
margin CDATA #IMPLIED
|
33
|
+
margin_top CDATA #IMPLIED
|
34
|
+
margin_right CDATA #IMPLIED
|
35
|
+
margin_bottom CDATA #IMPLIED
|
36
|
+
margin_left CDATA #IMPLIED
|
37
|
+
position (static|relative|absolute) #IMPLIED
|
38
|
+
width CDATA #IMPLIED
|
39
|
+
w CDATA #IMPLIED
|
40
|
+
x CDATA #IMPLIED
|
41
|
+
y CDATA #IMPLIED
|
42
|
+
z CDATA #IMPLIED"
|
43
|
+
>
|
44
|
+
<!ENTITY % border_attrs
|
45
|
+
"border CDATA #IMPLIED
|
46
|
+
border_top CDATA #IMPLIED
|
47
|
+
border_right CDATA #IMPLIED
|
48
|
+
border_bottom CDATA #IMPLIED
|
49
|
+
border_left CDATA #IMPLIED"
|
50
|
+
>
|
51
|
+
<!ENTITY % block_attrs
|
52
|
+
"%common_attrs;
|
53
|
+
%border_attrs;
|
54
|
+
align (left|center|right) #IMPLIED
|
55
|
+
background CDATA #IMPLIED
|
56
|
+
color CDATA #IMPLIED
|
57
|
+
padding CDATA #IMPLIED
|
58
|
+
padding_top CDATA #IMPLIED
|
59
|
+
padding_right CDATA #IMPLIED
|
60
|
+
padding_bottom CDATA #IMPLIED
|
61
|
+
padding_left CDATA #IMPLIED
|
62
|
+
vertical_align CDATA #IMPLIED
|
63
|
+
valign CDATA #IMPLIED"
|
64
|
+
>
|
65
|
+
<!ATTLIST dynamic_image
|
66
|
+
%block_attrs;
|
67
|
+
from_source CDATA #IMPLIED
|
68
|
+
save CDATA #IMPLIED
|
69
|
+
save_endless CDATA #IMPLIED
|
70
|
+
quality CDATA #IMPLIED
|
71
|
+
>
|
72
|
+
<!ATTLIST block %block_attrs;>
|
73
|
+
|
74
|
+
<!--- Image element -->
|
75
|
+
<!ELEMENT image (#PCDATA)>
|
76
|
+
<!ATTLIST image
|
77
|
+
%common_attrs;
|
78
|
+
alpha CDATA #IMPLIED
|
79
|
+
crop CDATA #IMPLIED
|
80
|
+
>
|
81
|
+
|
82
|
+
<!--- Table element -->
|
83
|
+
<!ELEMENT table (row|cell)*>
|
84
|
+
<!ATTLIST table
|
85
|
+
%common_attrs;
|
86
|
+
background CDATA #IMPLIED
|
87
|
+
%border_attrs;
|
88
|
+
cols CDATA #IMPLIED
|
89
|
+
>
|
90
|
+
<!ELEMENT row (cell)*>
|
91
|
+
<!ELEMENT cell %BlockElements;>
|
92
|
+
<!ATTLIST cell %block_attrs;>
|
93
|
+
|
94
|
+
|
95
|
+
<!-- Pango elements to ise in text element -->
|
96
|
+
<!ENTITY % PangoElements "(#PCDATA|span|b|big|i|s|sub|sup|small|tt|u)*">
|
97
|
+
<!ELEMENT span %PangoElements;>
|
98
|
+
<!ELEMENT b %PangoElements;>
|
99
|
+
<!ELEMENT big %PangoElements;>
|
100
|
+
<!ELEMENT i %PangoElements;>
|
101
|
+
<!ELEMENT s %PangoElements;>
|
102
|
+
<!ELEMENT sub %PangoElements;>
|
103
|
+
<!ELEMENT sup %PangoElements;>
|
104
|
+
<!ELEMENT small %PangoElements;>
|
105
|
+
<!ELEMENT tt %PangoElements;>
|
106
|
+
<!ELEMENT u %PangoElements;>
|
107
|
+
<!ATTLIST span
|
108
|
+
font CDATA #IMPLIED
|
109
|
+
font_desc CDATA #IMPLIED
|
110
|
+
font_family CDATA #IMPLIED
|
111
|
+
face CDATA #IMPLIED
|
112
|
+
font_size CDATA #IMPLIED
|
113
|
+
size CDATA #IMPLIED
|
114
|
+
font_style CDATA #IMPLIED
|
115
|
+
style CDATA #IMPLIED
|
116
|
+
font_weight CDATA #IMPLIED
|
117
|
+
weight CDATA #IMPLIED
|
118
|
+
font_variant CDATA #IMPLIED
|
119
|
+
variant CDATA #IMPLIED
|
120
|
+
font_stretch CDATA #IMPLIED
|
121
|
+
stretch CDATA #IMPLIED
|
122
|
+
foreground CDATA #IMPLIED
|
123
|
+
fgcolor CDATA #IMPLIED
|
124
|
+
color CDATA #IMPLIED
|
125
|
+
background CDATA #IMPLIED
|
126
|
+
bgcolor CDATA #IMPLIED
|
127
|
+
underline CDATA #IMPLIED
|
128
|
+
underline_color CDATA #IMPLIED
|
129
|
+
rise CDATA #IMPLIED
|
130
|
+
strikethrough CDATA #IMPLIED
|
131
|
+
strikethrough_color CDATA #IMPLIED
|
132
|
+
fallback CDATA #IMPLIED
|
133
|
+
lang CDATA #IMPLIED
|
134
|
+
leter_spacing CDATA #IMPLIED
|
135
|
+
gravity CDATA #IMPLIED
|
136
|
+
gravity_hint CDATA #IMPLIED
|
137
|
+
>
|
138
|
+
|
139
|
+
<!--- Text element -->
|
140
|
+
<!ELEMENT text %PangoElements;>
|
141
|
+
<!ATTLIST text
|
142
|
+
%common_attrs;
|
143
|
+
align CDATA #IMPLIED
|
144
|
+
auto_dir (true) #IMPLIED
|
145
|
+
color CDATA #IMPLIED
|
146
|
+
crop_to CDATA #IMPLIED
|
147
|
+
crop_suffix CDATA #IMPLIED
|
148
|
+
font CDATA #IMPLIED
|
149
|
+
indent CDATA #IMPLIED
|
150
|
+
justify CDATA #IMPLIED
|
151
|
+
spacng CDATA #IMPLIED
|
152
|
+
to_fit CDATA #IMPLIED
|
153
|
+
>
|
154
|
+
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
|
3
|
+
# Module contains parsers of dynamic images from static formats.
|
4
|
+
module DynamicImageParsers
|
5
|
+
# XML Parser parses XML documents containing dynamic images. You can give many images into one XML document.
|
6
|
+
#
|
7
|
+
# XML document has to be in same hierarchy as in pure ruby. Save, save_endless and treir quality option is given as dynamic_images's attribute.
|
8
|
+
#
|
9
|
+
# Save_endless images limit is gives as attribute too. It's name is :save_endless_limit.
|
10
|
+
#
|
11
|
+
# Save_endless filename has to be given as string attribute. You can set place of index by <tt>%{index}</tt>. F.e.: <tt>"image-%{index}.png"</tt>.
|
12
|
+
#
|
13
|
+
# Options are taken from element attributes. There is same rules like in pure ruby. See DynamicImage.
|
14
|
+
#
|
15
|
+
# === Example
|
16
|
+
#
|
17
|
+
# <?xml version="1.0" encoding="UTF-8" ?>
|
18
|
+
# <!DOCTYPE dynamic_images PUBLIC "-//malis//dynamic_images//EN" "https://raw.github.com/malis/dynamic_images/master/lib/parsers/xml.dtd">
|
19
|
+
# <dynamic_images>
|
20
|
+
# <dynamic_image from_source="earth.jpg" align="center" valign="middle" background="red 0.5" save="test_from.jpg" quality="90">
|
21
|
+
# <text font="Arial bold 15">testing <u>adding</u> some text to image</text>
|
22
|
+
# <block background="red">
|
23
|
+
# <image w="50" h="50">kostky.png</image>
|
24
|
+
# </block>
|
25
|
+
# <table>
|
26
|
+
# <row>
|
27
|
+
# <cell>
|
28
|
+
# <text><b>Left table header</b></text>
|
29
|
+
# </cell>
|
30
|
+
# <cell>
|
31
|
+
# <text><b>Right table header</b></text>
|
32
|
+
# </cell>
|
33
|
+
# </row>
|
34
|
+
# <row>
|
35
|
+
# <cell>
|
36
|
+
# <text>Table value 1</text>
|
37
|
+
# </cell>
|
38
|
+
# <cell>
|
39
|
+
# <text>Table value 2</text>
|
40
|
+
# </cell>
|
41
|
+
# </row>
|
42
|
+
# </table>
|
43
|
+
# </dynamic_image>
|
44
|
+
# </dynamic_images>
|
45
|
+
#
|
46
|
+
class XmlParser
|
47
|
+
# Accepts filename or +String+ containing XML document and processes it with dynamic_images library.
|
48
|
+
def initialize(filename_or_xml, render_only_first_to = nil, options = {})
|
49
|
+
@render_only_first_to = render_only_first_to
|
50
|
+
@options = options
|
51
|
+
filename_or_xml = File.read(filename_or_xml) if File.exists? filename_or_xml
|
52
|
+
doc = REXML::Document.new(filename_or_xml)
|
53
|
+
doc.elements.first.each_element do |image|
|
54
|
+
dynamic_image image
|
55
|
+
return if @render_only_first_to
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def dynamic_image(image)
|
61
|
+
options = get_options image
|
62
|
+
DynamicImage.new options do |dimg|
|
63
|
+
image.each_element do |xml_element|
|
64
|
+
in_block_element xml_element, dimg
|
65
|
+
end
|
66
|
+
if @render_only_first_to
|
67
|
+
save_options = @options[:quality] ? {:quality => @options[:quality]} : {}
|
68
|
+
save_options[:format] = @options[:format]
|
69
|
+
dimg.save! @render_only_first_to, save_options
|
70
|
+
else
|
71
|
+
save_options = options[:quality] ? {:quality => options[:quality]} : {}
|
72
|
+
if options[:save]
|
73
|
+
dimg.save! options[:save], save_options
|
74
|
+
elsif options[:save_endless]
|
75
|
+
dimg.save_endless! options[:save_endless_limit].to_i do |index|
|
76
|
+
options[:save_endless].gsub("%{index}", index)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def in_block_element(xml_element, block)
|
84
|
+
options = get_options xml_element
|
85
|
+
case xml_element.name.downcase
|
86
|
+
when "block"
|
87
|
+
block.block options do |block|
|
88
|
+
xml_element.each_element do |e|
|
89
|
+
in_block_element e, block
|
90
|
+
end
|
91
|
+
end
|
92
|
+
when "image"
|
93
|
+
block.image get_raw(xml_element), options
|
94
|
+
when "table"
|
95
|
+
block.table options do |table|
|
96
|
+
xml_element.each_element do |e|
|
97
|
+
in_table_element e, table
|
98
|
+
end
|
99
|
+
end
|
100
|
+
when "text"
|
101
|
+
block.text get_raw(xml_element), options
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def in_table_element(xml_element, table)
|
106
|
+
options = get_options xml_element
|
107
|
+
case xml_element.name.downcase
|
108
|
+
when "cell"
|
109
|
+
table.cell options do |block|
|
110
|
+
xml_element.each_element do |e|
|
111
|
+
in_block_element e, block
|
112
|
+
end
|
113
|
+
end
|
114
|
+
when "row"
|
115
|
+
table.row do |row|
|
116
|
+
xml_element.each_element do |e|
|
117
|
+
in_table_element e, row
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Basic parsing XML elements methods
|
124
|
+
|
125
|
+
def get_options(e)
|
126
|
+
options = {}
|
127
|
+
e.attributes.each {|k, v| options[k] = v }
|
128
|
+
options
|
129
|
+
end
|
130
|
+
|
131
|
+
def get_raw(e)
|
132
|
+
e.to_s.sub(/\A<[^>]*>/, '').sub(/<[^>]*>\Z/, '')
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/lib/render_image.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# Module used to add methods to ActionController::Base
|
2
|
+
module RenderImage
|
3
|
+
# Provides image drawing for Rails app controller. Default format is <tt>action_name.png</tt> in file called <tt>action_name.png.xml.erb</tt>.
|
4
|
+
#
|
5
|
+
# You can use different template name but it has to have extension <tt>.format.xml.erb</tt>. Passing extension <tt>.xml.erb</tt> is optional but file has be called with it.
|
6
|
+
#
|
7
|
+
# You can optionaly pass options (see below) and assigns too. Assigns is +Hash+ of keys and values which will be accessible from view as variables called as keys names.
|
8
|
+
#
|
9
|
+
# === Options
|
10
|
+
# [:quality]
|
11
|
+
# When saving into JPEG format you can pass :quality into options. Valid values are in 0 - 100.
|
12
|
+
#
|
13
|
+
# === Example
|
14
|
+
# File show.png.xml.erb should looks like this:
|
15
|
+
#
|
16
|
+
# <?xml version="1.0" encoding="UTF-8" ?>
|
17
|
+
# <!DOCTYPE dynamic_images PUBLIC "-//malis//dynamic_images//EN" "https://raw.github.com/malis/dynamic_images/master/lib/parsers/xml.dtd">
|
18
|
+
# <dynamic_images>
|
19
|
+
# <dynamic_image width="500" align="center" background="blue 0.5">
|
20
|
+
# <text font="Arial bold 20"><%= @article.title %></text>
|
21
|
+
# <text indent="30"><%= @article.text %></text>
|
22
|
+
# </dynamic_image>
|
23
|
+
# </dynamic_images>
|
24
|
+
#
|
25
|
+
# Attributes save, save_endless and quality will be omitted. You can pass quality in options +Hash+.
|
26
|
+
#
|
27
|
+
def render_image(template = nil, options = {}, assigns = {})
|
28
|
+
template ||= "#{action_name}.png"
|
29
|
+
template += ".xml.erb" if template.class == String && template !~ /\.xml\.erb\Z/i
|
30
|
+
view_path = nil
|
31
|
+
view_paths.each do |v_path|
|
32
|
+
v_path = File.join(v_path, controller_path)
|
33
|
+
view_path = v_path if File.exists? File.join(v_path, template)
|
34
|
+
end
|
35
|
+
raise "There is no template #{template} in paths: #{view_paths.join ' '}" unless view_path
|
36
|
+
|
37
|
+
view = ActionView::Base.new(view_path, assigns)
|
38
|
+
view.extend ApplicationHelper
|
39
|
+
instance_variables.each do |var|
|
40
|
+
next if var.to_s !~ /\A@[a-z]/i
|
41
|
+
view.instance_variable_set var, instance_variable_get(var)
|
42
|
+
end
|
43
|
+
xml = view.render(:file => template)
|
44
|
+
|
45
|
+
file = template.sub(/\.xml\.erb\Z/i, '')
|
46
|
+
if RUBY_VERSION >= "1.9"
|
47
|
+
tempfile = Tempfile.new file, :encoding => 'ascii-8bit'
|
48
|
+
else
|
49
|
+
tempfile = Tempfile.new file
|
50
|
+
end
|
51
|
+
options[:format] = file.scan(/\.([a-z0-9]+)\Z/i).flatten.first
|
52
|
+
|
53
|
+
DynamicImageParsers::XmlParser.new xml, tempfile, options
|
54
|
+
|
55
|
+
tempfile.rewind
|
56
|
+
send_data tempfile.read, :filename => file, :disposition => 'inline', :type => "image/#{options[:format]}"
|
57
|
+
tempfile.close
|
58
|
+
tempfile.unlink
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/source_factory.rb'
|
2
|
+
|
3
|
+
module DynamicImageSources
|
4
|
+
# Source providing solid color to use as source.
|
5
|
+
class ColorSource < SourceFactory
|
6
|
+
# Creates source object from <tt>Cairo::Color::RGB</tt> object and alpha as Float value.
|
7
|
+
def initialize(color, alpha)
|
8
|
+
alpha = nil unless alpha.class == Float
|
9
|
+
@red = color.red
|
10
|
+
@green = color.green
|
11
|
+
@blue = color.blue
|
12
|
+
@alpha = alpha || 1
|
13
|
+
end
|
14
|
+
|
15
|
+
# Gets color component
|
16
|
+
attr_reader :red, :green, :blue, :alpha
|
17
|
+
|
18
|
+
# Gives +Array+ of all known named colors. See http://cairo.rubyforge.org/doc/en/cairo-color.html#label-5
|
19
|
+
def self.named_colors
|
20
|
+
@@named_colors ||= (Cairo::Color.constants.sort - %w{ RGB CMYK HSV X11 Base HEX_RE } - %w{ RGB CMYK HSV X11 Base HEX_RE }.map(&:to_sym)).map(&:to_s)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns source object or nil if it can't parse it.
|
24
|
+
#
|
25
|
+
# === Supported syntax
|
26
|
+
# All values can be given as +Array+ or +String+ separated by space chars.
|
27
|
+
#
|
28
|
+
# To make color transparent add number value at the end of +Array+ or +String+.
|
29
|
+
#
|
30
|
+
# For any number value are valid values are 0 - 255 or 0.0 - 1.0
|
31
|
+
#
|
32
|
+
# [Name of color]
|
33
|
+
# Use one of ColorSource.named_colors.
|
34
|
+
# [RGB]
|
35
|
+
# Use separated number values for red, green and blue.
|
36
|
+
# [CMYK]
|
37
|
+
# Use :cmyk key as first value followed by separated number values for cyan, magenta, yellow and black.
|
38
|
+
# [HSV]
|
39
|
+
# Use :hsv key as first value followed by separated number values for hue, saturation and value.
|
40
|
+
# [HEX]
|
41
|
+
# Use +String+ starting with <tt>#</tt> char followed by 6 or 3 hex numbers. Hex numbers are doubled if only 3 hex numbers are given. Color <tt>#AABBCC</tt> is same as <tt>#ABC</tt>.
|
42
|
+
#
|
43
|
+
# === Example
|
44
|
+
# * <tt>:red</tt> is same as <tt>"red"</tt> and <tt>[:red]</tt>
|
45
|
+
# * <tt>[255, 0, 0]</tt> and <tt>"#FF0000"</tt> makes red color
|
46
|
+
# * <tt>[255, 0, 0, 64]</tt> and <tt>["#F00", 64]</tt> makes red color with 75% transparency
|
47
|
+
# * <tt>[1.0, 0, 0, 0.25]</tt> and <tt>"#F00 0.25"</tt> makes red color with 75% transparency
|
48
|
+
# * <tt>[:cmyk, 0, 0, 1.0, 0]</tt> makes yellow color
|
49
|
+
# * <tt>[:cmyk, 0, 0, 255, 0, 0.5]</tt> makes yellow color with 50% transparency
|
50
|
+
#
|
51
|
+
def self.parse(source)
|
52
|
+
return source if source.is_a? SourceFactory
|
53
|
+
if source[0].to_s =~ /^#([0-9a-f]{3}|[0-9a-f]{6})$/i
|
54
|
+
hex = ($1.size == 6 ? $1 : $1.unpack('AXAAXAAXA').join).unpack("A2A2A2")
|
55
|
+
source.shift
|
56
|
+
hex.reverse.each {|h| source.unshift h.to_i(16) }
|
57
|
+
end
|
58
|
+
if is_all_nums(source, 0..2)
|
59
|
+
treat_numbers source
|
60
|
+
new Cairo::Color::RGB.new(*source[0..2]), source[3]
|
61
|
+
elsif source[0] == "cmyk" && is_all_nums(source, 1..4)
|
62
|
+
treat_numbers source
|
63
|
+
new Cairo::Color::CMYK.new(*source[1..4]).to_rgb, source[5]
|
64
|
+
elsif source[0] == "hsv" && is_all_nums(source, 1..3)
|
65
|
+
treat_numbers source
|
66
|
+
new Cairo::Color::HSV.new(*source[1..3]).to_rgb, source[4]
|
67
|
+
elsif named_colors.include? source[0].to_s.upcase
|
68
|
+
treat_numbers source
|
69
|
+
new Cairo::Color.parse(source[0].to_s.upcase), source[1]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def self.is_all_nums(arr, int)
|
75
|
+
int.to_a.each do |index|
|
76
|
+
return false unless arr[index] && arr[index].to_s =~ /^\d+(\.\d+)?$/
|
77
|
+
end
|
78
|
+
return true
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.treat_numbers(source)
|
82
|
+
source.each_with_index do |value, index|
|
83
|
+
if source[index].to_s =~ /^\d+$/
|
84
|
+
source[index] = source[index].to_f/255.0
|
85
|
+
elsif source[index].to_s =~ /^\d+\.\d+$/
|
86
|
+
source[index] = source[index].to_f
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
public
|
92
|
+
# Sets color as source to given context
|
93
|
+
def set_source(context, x, y, w, h)
|
94
|
+
context.set_source_rgba @red, @green, @blue, @alpha
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/source_factory.rb'
|
2
|
+
require File.dirname(__FILE__) + '/color_source.rb'
|
3
|
+
|
4
|
+
module DynamicImageSources
|
5
|
+
# Source providing gradient source to drawing.
|
6
|
+
class GradientSource < SourceFactory
|
7
|
+
# Creates gradient source object from type and extend type.
|
8
|
+
#
|
9
|
+
# Valid values for type are :linear and :radial. Default is :linear.
|
10
|
+
#
|
11
|
+
# Valid values for extend type are :pad, :repeat and :reflect. Default is :pad.
|
12
|
+
#
|
13
|
+
def initialize(type, ext, *args)
|
14
|
+
@type = type || :linear
|
15
|
+
@ext = ext || :pad
|
16
|
+
@args = args
|
17
|
+
@stops = []
|
18
|
+
end
|
19
|
+
|
20
|
+
public
|
21
|
+
# Returns source object or nil if it can't parse it.
|
22
|
+
#
|
23
|
+
# === Supported syntax
|
24
|
+
# All values can be given as +Array+ or +String+ separated by space chars.
|
25
|
+
#
|
26
|
+
# Values has to be in this order: <tt>[:gradient_name, *gradient_arguments, stop_value1, stop_value2, ..., stop_valueN]</tt>. All values has to be in one-dimensional array.
|
27
|
+
#
|
28
|
+
# ==== Gradient name
|
29
|
+
# Valid gradient names are:
|
30
|
+
# * Linear:
|
31
|
+
# * <tt>:gradient</tt> for linear gradient
|
32
|
+
# * <tt>:gradient_repeat</tt> for linear gradient with repeating
|
33
|
+
# * <tt>:gradient_reflect</tt> for linear gradient with reflection
|
34
|
+
# * Radial:
|
35
|
+
# * <tt>:gradient_radial</tt> for radial gradient
|
36
|
+
# * <tt>:gradient_radial_repeat</tt> for radial gradient with repeating
|
37
|
+
# * <tt>:gradient_radial_reflect</tt> for radial gradient with reflection
|
38
|
+
#
|
39
|
+
# ==== Gradient arguments
|
40
|
+
# Gradient arguments are different for linear and radial gradients. Adding gradient options is optional.
|
41
|
+
#
|
42
|
+
# ===== Linear
|
43
|
+
# Valid values are <tt>[x0, y0, x1, y1]</tt> or <tt>[angle, size]</tt> where size is optional.
|
44
|
+
#
|
45
|
+
# At first place x0, y0 locates first and x1, y1 second points of gradient vector in pixels.
|
46
|
+
#
|
47
|
+
# At second place angle defines direction of gradient from top left corner. Valid values are 0deg - 360deg ("deg" must be included). Default is 0deg meaning East. Angle is counted in clockwise direction.
|
48
|
+
#
|
49
|
+
# Size is a length of gradinet vector. You can use percentage. F.e.: <tt>"50%"</tt>.
|
50
|
+
#
|
51
|
+
# ===== Radial
|
52
|
+
# Valid values are <tt>[x0, y0, r0, x1, y1, r1]</tt> or <tt>[size, angle0, dif0, angle1, dif1]</tt> where size and pair angle1, dif1 are optional.
|
53
|
+
#
|
54
|
+
# At first place x0, y0 locates first and x1, y1 second centers of circles and r0, r1 theirs radius. All in pixels.
|
55
|
+
#
|
56
|
+
# At second place size is a length of gradinet vector. You can use percentage. F.e.: <tt>"50%"</tt>.
|
57
|
+
#
|
58
|
+
# Angle and dif defines direction and value of circle movement. Valid values for angle are 0deg - 360deg ("deg" must be included). Default is 0deg meaning East. Angle is counted in clockwise direction. You can use percentage for dif. F.e.: <tt>"50%"</tt>.
|
59
|
+
#
|
60
|
+
# ==== Stop values
|
61
|
+
# Stop is described as offset value at first position followed by color. The offset specifies the location in % along the gradient's control vector. For color see ColorSource.parse.
|
62
|
+
#
|
63
|
+
# === Example
|
64
|
+
# * <tt>[:gradient, "0%", :red, "100%", :blue]</tt> will create linear gradient red at left side and blue at right side
|
65
|
+
# * <tt>[:gradient, "0%", 1, 0, 0, "100%", 0, 0, 1]</tt> same as above
|
66
|
+
# * <tt>[:gradient_reflect, "50%", "0%", 1, 0, 0, "100%", 0, 0, 1]</tt> will create gradient red at left side, blue in middle and red at right side
|
67
|
+
# * <tt>[:gradient, "90deg", "0%", :red, "100%", :blue]</tt> will create linear gradient red at top and blue at bottom
|
68
|
+
# * <tt>[:gradient_radial, "0%", :red, "100%", :blue]</tt> will create radial gradient red in center and blue at sides
|
69
|
+
# * <tt>[:gradient_radial, 100, "0%", :red, "100%", :blue]</tt> will create radial gradient red in center and blue 100px from center
|
70
|
+
# * <tt>[:gradient_radial, "225deg", "50%", "0%", :red, "100%", :blue]</tt>will create radial gradient red in left top quadrant and blue at sides
|
71
|
+
#
|
72
|
+
def self.parse(source)
|
73
|
+
if source[0].to_s.downcase =~ /\Agradient_?(radial)?_?(reflect|repeat)?\Z/
|
74
|
+
ext = $2
|
75
|
+
source.shift
|
76
|
+
unless $1 #linear
|
77
|
+
if source[0..3].join(' ') =~ /\A(\d+) (\d+) (\d+) (\d+)\Z/ # x0, y0, x1, y1
|
78
|
+
args = [$1, $2, $3, $4].map(&:to_i)
|
79
|
+
source.shift 4
|
80
|
+
else # angle deg, [size (%)]
|
81
|
+
args = [0, 1.0]
|
82
|
+
if source.first.to_s =~ /\A\d+deg\Z/ #angle
|
83
|
+
args[0] = source.first.to_i
|
84
|
+
source.shift
|
85
|
+
end
|
86
|
+
if source.first.to_s =~ /\A\d+\Z/ #size
|
87
|
+
args[1] = source.first.to_i
|
88
|
+
source.shift
|
89
|
+
end
|
90
|
+
if source[1].to_s =~ /\A\d+%\Z/ && source.first.to_s =~ /\A(\d+)%\Z/ # size
|
91
|
+
args[1] = $1.to_f/100.0
|
92
|
+
source.shift
|
93
|
+
end
|
94
|
+
end
|
95
|
+
object = new :linear, ext, *args
|
96
|
+
else #radial
|
97
|
+
# Args => [size (%),] [angle1 deg, dif1 (%), [angle2 deg, dif2 (%)]]
|
98
|
+
if source[0..5].join(' ') =~ /\A(\d+) (\d+) (\d+) (\d+) (\d+) (\d+)\Z/ # x0, y0, d0, x1, y1, d1
|
99
|
+
args = [$1, $2, $3, $4, $5, $6].map(&:to_i)
|
100
|
+
source.shift 6
|
101
|
+
else
|
102
|
+
args = [1.0, 0, 0, 0, 0]
|
103
|
+
if source.first.to_s =~ /\A\d+\Z/ #size
|
104
|
+
args[0] = source.first.to_i
|
105
|
+
source.shift
|
106
|
+
end
|
107
|
+
if source[1].to_s =~ /\A\d+(%|deg)\Z/ && source.first.to_s =~ /\A(\d+)%\Z/ # size
|
108
|
+
args[0] = $1.to_f/100.0
|
109
|
+
source.shift
|
110
|
+
end
|
111
|
+
if source[0..1].join(' ') =~ /\A(\d+)deg (\d+)(%)?\Z/ # angle1 deg, dif1 (%)
|
112
|
+
args[1] = $1.to_i
|
113
|
+
args[2] = $3 ? $2.to_f/100.0 : $2.to_i
|
114
|
+
source.shift 2
|
115
|
+
end
|
116
|
+
if source[0..1].join(' ') =~ /\A(\d+)deg (\d+)(%)?\Z/ # angle2 deg, dif2 (%)
|
117
|
+
args[3] = $1.to_i
|
118
|
+
args[4] = $3 ? $2.to_f/100.0 : $2.to_i
|
119
|
+
source.shift 2
|
120
|
+
end
|
121
|
+
end
|
122
|
+
object = new :radial, ext, *args
|
123
|
+
end
|
124
|
+
stops = []
|
125
|
+
source.each do |i|
|
126
|
+
if i.to_s =~ /(\d+)%/
|
127
|
+
stops << [$1.to_f/100.0]
|
128
|
+
else
|
129
|
+
stops.last << i if stops.last
|
130
|
+
end
|
131
|
+
end
|
132
|
+
stops.each do |stop|
|
133
|
+
color = ColorSource.parse(stop[1..-1])
|
134
|
+
return nil unless color
|
135
|
+
object.send :add_stop, stop.first, color
|
136
|
+
end
|
137
|
+
object
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Sets color as source to given context
|
142
|
+
def set_source(context, x, y, w, h)
|
143
|
+
case @type.to_sym
|
144
|
+
when :linear
|
145
|
+
if @args.size == 4
|
146
|
+
pattern = Cairo::LinearPattern.new @args[0]+x, @args[1]+y, @args[2]+x, @args[3]+y
|
147
|
+
else
|
148
|
+
if @args[1].class == Float
|
149
|
+
deg = (@args[0]%180+180)%180
|
150
|
+
deg = 180 - deg if deg > 90
|
151
|
+
deg *= Math::PI / 180
|
152
|
+
deg -= Math.atan(h.to_f/w.to_f)
|
153
|
+
dist = Math.sqrt(w**2 + h**2) * Math.cos(deg)
|
154
|
+
dist = dist * @args[1]
|
155
|
+
else
|
156
|
+
dist = @args[1]
|
157
|
+
end
|
158
|
+
pattern = Cairo::LinearPattern.new *[x, y, degree_dist(@args[0], dist, x, y)].flatten
|
159
|
+
end
|
160
|
+
when :radial
|
161
|
+
if @args.size == 6
|
162
|
+
pattern = Cairo::RadialPattern.new @args[0]+x, @args[1]+y, @args[2], @args[3]+x, @args[4]+y, @args[5]
|
163
|
+
else
|
164
|
+
x, y = [x+w/2, y+h/2]
|
165
|
+
radius = @args[0].class == Float ? Math.sqrt(w**2 + h**2)/2 * @args[0] : @args[0]
|
166
|
+
dist1 = @args[2].class == Float ? radius * @args[2] : @args[2]
|
167
|
+
dist2 = @args[4].class == Float ? radius * @args[4] : @args[4]
|
168
|
+
pattern = Cairo::RadialPattern.new *[degree_dist(@args[1], dist1, x, y), 0, degree_dist(@args[3], dist2, x, y), radius].flatten
|
169
|
+
end
|
170
|
+
end
|
171
|
+
pattern.set_extend({
|
172
|
+
:pad => Cairo::EXTEND_NONE,
|
173
|
+
:repeat => Cairo::EXTEND_REPEAT,
|
174
|
+
:reflect => Cairo::EXTEND_REFLECT
|
175
|
+
}[@ext.to_sym]) unless @type.to_sym == :radial && @ext.to_sym == :pad
|
176
|
+
@stops.each do |stop|
|
177
|
+
pattern.add_color_stop_rgba *stop
|
178
|
+
end
|
179
|
+
|
180
|
+
context.set_source pattern
|
181
|
+
context.fill_preserve
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
def add_stop(offset, color_source)
|
186
|
+
@stops << [offset, color_source.red, color_source.green, color_source.blue, color_source.alpha]
|
187
|
+
end
|
188
|
+
|
189
|
+
def degree_dist(deg, dist, x, y)
|
190
|
+
return [x, y] if dist.zero?
|
191
|
+
deg = (deg%360+360)%360
|
192
|
+
return [x+dist, y] if deg == 0
|
193
|
+
return [x, y+dist] if deg == 90
|
194
|
+
return [x-dist, y] if deg == 180
|
195
|
+
return [x, y-dist] if deg == 270
|
196
|
+
deg *= Math::PI / 180
|
197
|
+
return [x+dist*Math.cos(deg), y+dist*Math.sin(deg)]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|