dynamic_images 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|