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