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,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
@@ -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