prawn-svg 0.9.1.6 → 0.9.1.7

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/lib/prawn-svg.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'prawn'
2
- require 'prawn/svg_document'
3
- require 'prawn/svg/svg'
2
+ require 'prawn/svg/extension'
3
+ require 'prawn/svg/interface'
4
4
  require 'prawn/svg/parser'
5
- require 'prawn/svg/path'
5
+ require 'prawn/svg/parser/path'
6
+
7
+ Prawn::Document.extensions << Prawn::Svg::Extension
@@ -0,0 +1,24 @@
1
+ module Prawn
2
+ module Svg
3
+ module Extension
4
+ #
5
+ # Draws an SVG document into the PDF.
6
+ #
7
+ # +options+ must contain the key :at, which takes a tuple of x and y co-ordinates.
8
+ #
9
+ # +options+ can optionally contain the key :width or :height. If both are
10
+ # specified, only :width will be used. If neither are specified, the resolution
11
+ # given in the SVG will be used.
12
+ #
13
+ # Example usage:
14
+ #
15
+ # svg IO.read("example.svg"), :at => [100, 300], :width => 600
16
+ #
17
+ def svg(data, options={})
18
+ svg = Prawn::Svg::Interface.new(data, self, options)
19
+ svg.draw
20
+ {:warnings => svg.parser_warnings, :width => svg.parser.width, :height => svg.parser.height}
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,87 @@
1
+ #
2
+ # Prawn::Svg::Interface makes a Prawn::Svg::Parser instance, uses that object to parse the supplied
3
+ # SVG into Prawn-compatible method calls, and then calls the Prawn methods.
4
+ #
5
+ module Prawn
6
+ module Svg
7
+ class Interface
8
+ DEFAULT_FONT_PATHS = ["/Library/Fonts", "/usr/share/fonts/truetype/**"]
9
+
10
+ @font_path = []
11
+ DEFAULT_FONT_PATHS.each {|path| @font_path << path if File.exists?(path)}
12
+
13
+ class << self; attr_accessor :font_path; end
14
+
15
+ attr_reader :data, :prawn, :parser, :options
16
+
17
+ # An +Array+ of warnings that occurred while parsing the SVG data. If this array is non-empty,
18
+ # it's likely that the SVG failed to render correctly.
19
+ attr_reader :parser_warnings
20
+
21
+ #
22
+ # Creates a Prawn::Svg object.
23
+ #
24
+ # +data+ is the SVG data to convert. +prawn+ is your Prawn::Document object.
25
+ #
26
+ # +options+ must contain the key :at, which takes a tuple of x and y co-ordinates.
27
+ #
28
+ # +options+ can optionally contain the key :width or :height. If both are
29
+ # specified, only :width will be used.
30
+ #
31
+ def initialize(data, prawn, options)
32
+ @data = data
33
+ @prawn = prawn
34
+ @options = options
35
+
36
+ @options[:at] or raise "options[:at] must be specified"
37
+
38
+ @parser = Parser.new(data, [prawn.bounds.width, prawn.bounds.height], options)
39
+ @parser_warnings = @parser.warnings
40
+ end
41
+
42
+ #
43
+ # Draws the SVG to the Prawn::Document object.
44
+ #
45
+ def draw
46
+ prawn.bounding_box(@options[:at], :width => @parser.width, :height => @parser.height) do
47
+ prawn.save_graphics_state do
48
+ proc_creator(prawn, @parser.parse).call
49
+ end
50
+ end
51
+ end
52
+
53
+
54
+ private
55
+ def proc_creator(prawn, calls)
56
+ Proc.new {issue_prawn_command(prawn, calls)}
57
+ end
58
+
59
+ def issue_prawn_command(prawn, calls)
60
+ calls.each do |call, arguments, children|
61
+ if rewrite_call_arguments(prawn, call, arguments) == false
62
+ issue_prawn_command(prawn, children) if children.any?
63
+ else
64
+ if children.empty?
65
+ prawn.send(call, *arguments)
66
+ else
67
+ prawn.send(call, *arguments, &proc_creator(prawn, children))
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def rewrite_call_arguments(prawn, call, arguments)
74
+ case call
75
+ when 'text_box'
76
+ if (anchor = arguments.last.delete(:text_anchor)) && %w(middle end).include?(anchor)
77
+ width = prawn.width_of(*arguments)
78
+ width /= 2 if anchor == 'middle'
79
+ arguments.last[:at][0] -= width
80
+ end
81
+
82
+ arguments.last[:at][1] += prawn.height_of(*arguments) / 3 * 2
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -11,441 +11,447 @@ require 'rexml/document'
11
11
  # prawn specifically - this might be useful if you want to take this code and use it to convert
12
12
  # SVG to another format.
13
13
  #
14
- class Prawn::Svg::Parser
15
- begin
16
- require 'css_parser'
17
- CSS_PARSER_LOADED = true
18
- rescue LoadError
19
- CSS_PARSER_LOADED = false
20
- end
14
+ module Prawn
15
+ module Svg
16
+ class Parser
17
+ begin
18
+ require 'css_parser'
19
+ CSS_PARSER_LOADED = true
20
+ rescue LoadError
21
+ CSS_PARSER_LOADED = false
22
+ end
21
23
 
22
- include Prawn::Measurements
24
+ include Prawn::Measurements
23
25
 
24
- attr_reader :width, :height
26
+ attr_reader :width, :height
25
27
 
26
- # An +Array+ of warnings that occurred while parsing the SVG data.
27
- attr_reader :warnings
28
+ # An +Array+ of warnings that occurred while parsing the SVG data.
29
+ attr_reader :warnings
28
30
 
29
- # The scaling factor, as determined by the :width or :height options.
30
- attr_accessor :scale
31
+ # The scaling factor, as determined by the :width or :height options.
32
+ attr_accessor :scale
31
33
 
32
- #
33
- # Construct a Parser object.
34
- #
35
- # The +data+ argument is SVG data.
36
- #
37
- # +bounds+ is a tuple [width, height] that specifies the bounds of the drawing space in points.
38
- #
39
- # +options+ can optionally contain
40
- # the key :width or :height. If both are specified, only :width will be used.
41
- #
42
- def initialize(data, bounds, options)
43
- @data = data
44
- @bounds = bounds
45
- @options = options
46
- @warnings = []
47
- @css_parser = CssParser::Parser.new if CSS_PARSER_LOADED
34
+ #
35
+ # Construct a Parser object.
36
+ #
37
+ # The +data+ argument is SVG data.
38
+ #
39
+ # +bounds+ is a tuple [width, height] that specifies the bounds of the drawing space in points.
40
+ #
41
+ # +options+ can optionally contain
42
+ # the key :width or :height. If both are specified, only :width will be used.
43
+ #
44
+ def initialize(data, bounds, options)
45
+ @data = data
46
+ @bounds = bounds
47
+ @options = options
48
+ @warnings = []
49
+ @css_parser = CssParser::Parser.new if CSS_PARSER_LOADED
48
50
 
49
- if data
50
- parse_document
51
- calculate_dimensions
52
- end
53
- end
51
+ if data
52
+ parse_document
53
+ calculate_dimensions
54
+ end
55
+ end
54
56
 
55
- #
56
- # Parse the SVG data and return a call tree. The returned +Array+ is in the format:
57
- #
58
- # [
59
- # ['prawn_method_name', ['argument1', 'argument2'], []],
60
- # ['method_that_takes_a_block', ['argument1', 'argument2'], [
61
- # ['method_called_inside_block', ['argument'], []]
62
- # ]
63
- # ]
64
- #
65
- def parse
66
- @warnings = []
67
- [].tap {|calls| parse_element(@root, calls, {})}
68
- end
57
+ #
58
+ # Parse the SVG data and return a call tree. The returned +Array+ is in the format:
59
+ #
60
+ # [
61
+ # ['prawn_method_name', ['argument1', 'argument2'], []],
62
+ # ['method_that_takes_a_block', ['argument1', 'argument2'], [
63
+ # ['method_called_inside_block', ['argument'], []]
64
+ # ]
65
+ # ]
66
+ #
67
+ def parse
68
+ @warnings = []
69
+ calls = []
70
+ parse_element(@root, calls, {})
71
+ calls
72
+ end
69
73
 
70
74
 
71
- private
72
- def parse_document
73
- @root = REXML::Document.new(@data).root
74
- @actual_width, @actual_height = @bounds # set this first so % width/heights can be used
75
+ private
76
+ def parse_document
77
+ @root = REXML::Document.new(@data).root
78
+ @actual_width, @actual_height = @bounds # set this first so % width/heights can be used
75
79
 
76
- if vb = @root.attributes['viewBox']
77
- x1, y1, x2, y2 = vb.strip.split(/\s+/)
78
- @x_offset, @y_offset = [x1.to_f, y1.to_f]
79
- @actual_width, @actual_height = [x2.to_f - x1.to_f, y2.to_f - y1.to_f]
80
- else
81
- @x_offset, @y_offset = [0, 0]
82
- @actual_width = points(@root.attributes['width'], :x)
83
- @actual_height = points(@root.attributes['height'], :y)
84
- end
85
- end
80
+ if vb = @root.attributes['viewBox']
81
+ x1, y1, x2, y2 = vb.strip.split(/\s+/)
82
+ @x_offset, @y_offset = [x1.to_f, y1.to_f]
83
+ @actual_width, @actual_height = [x2.to_f - x1.to_f, y2.to_f - y1.to_f]
84
+ else
85
+ @x_offset, @y_offset = [0, 0]
86
+ @actual_width = points(@root.attributes['width'], :x)
87
+ @actual_height = points(@root.attributes['height'], :y)
88
+ end
89
+ end
86
90
 
87
- REQUIRED_ATTRIBUTES = {
88
- "line" => %w(x1 y1 x2 y2),
89
- "polyline" => %w(points),
90
- "polygon" => %w(points),
91
- "circle" => %w(r),
92
- "ellipse" => %w(rx ry),
93
- "rect" => %w(x y width height),
94
- "path" => %w(d)
95
- }
91
+ REQUIRED_ATTRIBUTES = {
92
+ "line" => %w(x1 y1 x2 y2),
93
+ "polyline" => %w(points),
94
+ "polygon" => %w(points),
95
+ "circle" => %w(r),
96
+ "ellipse" => %w(rx ry),
97
+ "rect" => %w(x y width height),
98
+ "path" => %w(d)
99
+ }
96
100
 
97
- def parse_element(element, calls, state)
98
- attrs = element.attributes
99
- calls, style_attrs = apply_styles(element, calls, state)
101
+ def parse_element(element, calls, state)
102
+ attrs = element.attributes
103
+ calls, style_attrs = apply_styles(element, calls, state)
100
104
 
101
- if required_attributes = REQUIRED_ATTRIBUTES[element.name]
102
- return unless check_attrs_present(element, required_attributes)
103
- end
105
+ if required_attributes = REQUIRED_ATTRIBUTES[element.name]
106
+ return unless check_attrs_present(element, required_attributes)
107
+ end
104
108
 
105
- case element.name
106
- when 'title', 'desc'
107
- # ignore
109
+ case element.name
110
+ when 'title', 'desc'
111
+ # ignore
108
112
 
109
- when 'g', 'svg'
110
- element.elements.each do |child|
111
- parse_element(child, calls, state.dup)
112
- end
113
+ when 'g', 'svg'
114
+ element.elements.each do |child|
115
+ parse_element(child, calls, state.dup)
116
+ end
113
117
 
114
- when 'defs'
115
- # Pass calls as a blank array so that nothing under this tag can be added to our call tree.
116
- element.elements.each do |child|
117
- parse_element(child, [], state.dup.merge(:display => false))
118
- end
118
+ when 'defs'
119
+ # Pass calls as a blank array so that nothing under this tag can be added to our call tree.
120
+ element.elements.each do |child|
121
+ parse_element(child, [], state.dup.merge(:display => false))
122
+ end
119
123
 
120
- when 'style'
121
- load_css_styles(element)
124
+ when 'style'
125
+ load_css_styles(element)
122
126
 
123
- when 'text'
124
- # Very primitive support for font-family; it won't work in most cases because
125
- # PDF only has a few built-in fonts, and they're not the same as the names
126
- # used typically with the web fonts.
127
- if font_family = style_attrs["font-family"]
128
- if font_family != "" && pdf_font = map_font_family_to_pdf_font(font_family)
129
- calls << ['font', [pdf_font], []]
130
- calls = calls.last.last
131
- else
132
- @warnings << "#{font_family} is not a known font."
133
- end
134
- end
127
+ when 'text'
128
+ # Very primitive support for font-family; it won't work in most cases because
129
+ # PDF only has a few built-in fonts, and they're not the same as the names
130
+ # used typically with the web fonts.
131
+ if font_family = style_attrs["font-family"]
132
+ if font_family != "" && pdf_font = map_font_family_to_pdf_font(font_family)
133
+ calls << ['font', [pdf_font], []]
134
+ calls = calls.last.last
135
+ else
136
+ @warnings << "#{font_family} is not a known font."
137
+ end
138
+ end
135
139
 
136
- opts = {:at => [x(attrs['x']), y(attrs['y'])]}
137
- if size = style_attrs['font-size']
138
- opts[:size] = size.to_f * @scale
139
- end
140
+ opts = {:at => [x(attrs['x']), y(attrs['y'])]}
141
+ if size = style_attrs['font-size']
142
+ opts[:size] = size.to_f * @scale
143
+ end
140
144
 
141
- # This is not a prawn option but we can't work out how to render it here -
142
- # it's handled by Svg#rewrite_call_arguments
143
- if anchor = style_attrs['text-anchor']
144
- opts[:text_anchor] = anchor
145
- end
145
+ # This is not a prawn option but we can't work out how to render it here -
146
+ # it's handled by Svg#rewrite_call_arguments
147
+ if anchor = style_attrs['text-anchor']
148
+ opts[:text_anchor] = anchor
149
+ end
146
150
 
147
- calls << ['text_box', [element.text, opts], []]
151
+ calls << ['text_box', [element.text, opts], []]
148
152
 
149
- when 'line'
150
- calls << ['line', [x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])], []]
153
+ when 'line'
154
+ calls << ['line', [x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])], []]
151
155
 
152
- when 'polyline'
153
- points = attrs['points'].split(/\s+/)
154
- return unless base_point = points.shift
155
- x, y = base_point.split(",")
156
- calls << ['move_to', [x(x), y(y)], []]
157
- calls << ['stroke', [], []]
158
- calls = calls.last.last
159
- points.each do |point|
160
- x, y = point.split(",")
161
- calls << ["line_to", [x(x), y(y)], []]
162
- end
156
+ when 'polyline'
157
+ points = attrs['points'].split(/\s+/)
158
+ return unless base_point = points.shift
159
+ x, y = base_point.split(",")
160
+ calls << ['move_to', [x(x), y(y)], []]
161
+ calls << ['stroke', [], []]
162
+ calls = calls.last.last
163
+ points.each do |point|
164
+ x, y = point.split(",")
165
+ calls << ["line_to", [x(x), y(y)], []]
166
+ end
163
167
 
164
- when 'polygon'
165
- points = attrs['points'].split(/\s+/).collect do |point|
166
- x, y = point.split(",")
167
- [x(x), y(y)]
168
- end
169
- calls << ["polygon", points, []]
168
+ when 'polygon'
169
+ points = attrs['points'].split(/\s+/).collect do |point|
170
+ x, y = point.split(",")
171
+ [x(x), y(y)]
172
+ end
173
+ calls << ["polygon", points, []]
170
174
 
171
- when 'circle'
172
- calls << ["circle_at",
173
- [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}],
174
- []]
175
+ when 'circle'
176
+ calls << ["circle_at",
177
+ [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}],
178
+ []]
175
179
 
176
- when 'ellipse'
177
- calls << ["ellipse_at",
178
- [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])],
179
- []]
180
+ when 'ellipse'
181
+ calls << ["ellipse_at",
182
+ [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])],
183
+ []]
180
184
 
181
- when 'rect'
182
- radius = distance(attrs['rx'] || attrs['ry'])
183
- args = [[x(attrs['x']), y(attrs['y'])], distance(attrs['width']), distance(attrs['height'])]
184
- if radius
185
- # n.b. does not support both rx and ry being specified with different values
186
- calls << ["rounded_rectangle", args + [radius], []]
187
- else
188
- calls << ["rectangle", args, []]
189
- end
185
+ when 'rect'
186
+ radius = distance(attrs['rx'] || attrs['ry'])
187
+ args = [[x(attrs['x']), y(attrs['y'])], distance(attrs['width']), distance(attrs['height'])]
188
+ if radius
189
+ # n.b. does not support both rx and ry being specified with different values
190
+ calls << ["rounded_rectangle", args + [radius], []]
191
+ else
192
+ calls << ["rectangle", args, []]
193
+ end
190
194
 
191
- when 'path'
192
- @svg_path ||= Path.new
195
+ when 'path'
196
+ @svg_path ||= Path.new
193
197
 
194
- begin
195
- commands = @svg_path.parse(attrs['d'])
196
- rescue Prawn::Svg::Parser::Path::InvalidError => e
197
- commands = []
198
- @warnings << e.message
199
- end
198
+ begin
199
+ commands = @svg_path.parse(attrs['d'])
200
+ rescue Prawn::Svg::Parser::Path::InvalidError => e
201
+ commands = []
202
+ @warnings << e.message
203
+ end
200
204
 
201
- commands.each do |command, args|
202
- point_to = [x(args[0]), y(args[1])]
203
- if command == 'curve_to'
204
- bounds = [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]
205
- calls << [command, [point_to, {:bounds => bounds}], []]
206
- else
207
- calls << [command, point_to, []]
205
+ commands.each do |command, args|
206
+ point_to = [x(args[0]), y(args[1])]
207
+ if command == 'curve_to'
208
+ bounds = [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]
209
+ calls << [command, [point_to, {:bounds => bounds}], []]
210
+ else
211
+ calls << [command, point_to, []]
212
+ end
213
+ end
214
+
215
+ else
216
+ @warnings << "Unknown tag '#{element.name}'; ignoring"
208
217
  end
209
218
  end
210
-
211
- else
212
- @warnings << "Unknown tag '#{element.name}'; ignoring"
213
- end
214
- end
215
219
 
216
- def load_css_styles(element)
217
- if @css_parser
218
- data = if element.cdatas.any?
219
- element.cdatas.collect(&:to_s).join
220
- else
221
- element.text
222
- end
220
+ def load_css_styles(element)
221
+ if @css_parser
222
+ data = if element.cdatas.any?
223
+ element.cdatas.collect(&:to_s).join
224
+ else
225
+ element.text
226
+ end
223
227
 
224
- @css_parser.add_block!(data)
225
- end
226
- end
228
+ @css_parser.add_block!(data)
229
+ end
230
+ end
227
231
 
228
- def parse_css_declarations(declarations)
229
- # copied from css_parser
230
- declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')
232
+ def parse_css_declarations(declarations)
233
+ # copied from css_parser
234
+ declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')
231
235
 
232
- {}.tap do |o|
233
- declarations.split(/[\;$]+/m).each do |decs|
234
- if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
235
- property, value, end_of_declaration = matches.captures
236
- o[property] = value
236
+ output = {}
237
+ declarations.split(/[\;$]+/m).each do |decs|
238
+ if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
239
+ property, value, end_of_declaration = matches.captures
240
+ output[property] = value
241
+ end
237
242
  end
243
+ output
238
244
  end
239
- end
240
- end
241
245
 
242
- def determine_style_for(element)
243
- if @css_parser
244
- tag_style = @css_parser.find_by_selector(element.name)
245
- id_style = @css_parser.find_by_selector("##{element.attributes["id"]}") if element.attributes["id"]
246
+ def determine_style_for(element)
247
+ if @css_parser
248
+ tag_style = @css_parser.find_by_selector(element.name)
249
+ id_style = @css_parser.find_by_selector("##{element.attributes["id"]}") if element.attributes["id"]
246
250
 
247
- if classes = element.attributes["class"]
248
- class_styles = classes.strip.split(/\s+/).collect do |class_name|
249
- @css_parser.find_by_selector(".#{class_name}")
250
- end
251
- end
251
+ if classes = element.attributes["class"]
252
+ class_styles = classes.strip.split(/\s+/).collect do |class_name|
253
+ @css_parser.find_by_selector(".#{class_name}")
254
+ end
255
+ end
252
256
 
253
- element_style = element.attributes['style']
257
+ element_style = element.attributes['style']
254
258
 
255
- style = [tag_style, class_styles, id_style, element_style].flatten.collect do |s|
256
- s.nil? || s.strip == "" ? "" : "#{s}#{";" unless s.match(/;\s*\z/)}"
257
- end.join
258
- else
259
- style = element.attributes['style'] || ""
260
- end
261
-
262
- decs = parse_css_declarations(style)
263
- element.attributes.each {|n,v| decs[n] = v unless decs[n]}
264
- decs
265
- end
266
-
267
- def apply_styles(element, calls, state)
268
- decs = determine_style_for(element)
269
- draw_types = []
270
-
271
- # Transform
272
- if transform = decs['transform']
273
- parse_css_method_calls(transform).each do |name, arguments|
274
- case name
275
- when 'translate'
276
- x, y = arguments
277
- x, y = x.split(/\s+/) if y.nil?
278
- calls << [name, [distance(x), -distance(y)], []]
279
- calls = calls.last.last
280
- when 'rotate'
281
- calls << [name, [-arguments.first.to_f, {:origin => [0, y('0')]}], []]
282
- calls = calls.last.last
283
- when 'scale'
284
- calls << [name, [arguments.first.to_f], []]
285
- calls = calls.last.last
259
+ style = [tag_style, class_styles, id_style, element_style].flatten.collect do |s|
260
+ s.nil? || s.strip == "" ? "" : "#{s}#{";" unless s.match(/;\s*\z/)}"
261
+ end.join
286
262
  else
287
- @warnings << "Unknown transformation '#{name}'; ignoring"
263
+ style = element.attributes['style'] || ""
288
264
  end
265
+
266
+ decs = parse_css_declarations(style)
267
+ element.attributes.each {|n,v| decs[n] = v unless decs[n]}
268
+ decs
289
269
  end
290
- end
270
+
271
+ def apply_styles(element, calls, state)
272
+ decs = determine_style_for(element)
273
+ draw_types = []
274
+
275
+ # Transform
276
+ if transform = decs['transform']
277
+ parse_css_method_calls(transform).each do |name, arguments|
278
+ case name
279
+ when 'translate'
280
+ x, y = arguments
281
+ x, y = x.split(/\s+/) if y.nil?
282
+ calls << [name, [distance(x), -distance(y)], []]
283
+ calls = calls.last.last
284
+ when 'rotate'
285
+ calls << [name, [-arguments.first.to_f, {:origin => [0, y('0')]}], []]
286
+ calls = calls.last.last
287
+ when 'scale'
288
+ calls << [name, [arguments.first.to_f], []]
289
+ calls = calls.last.last
290
+ else
291
+ @warnings << "Unknown transformation '#{name}'; ignoring"
292
+ end
293
+ end
294
+ end
291
295
 
292
- # Opacity:
293
- # We can't do nested opacities quite like the SVG requires, but this is close enough.
294
- fill_opacity = stroke_opacity = clamp(decs['opacity'].to_f, 0, 1) if decs['opacity']
295
- fill_opacity = clamp(decs['fill-opacity'].to_f, 0, 1) if decs['fill-opacity']
296
- stroke_opacity = clamp(decs['stroke-opacity'].to_f, 0, 1) if decs['stroke-opacity']
296
+ # Opacity:
297
+ # We can't do nested opacities quite like the SVG requires, but this is close enough.
298
+ fill_opacity = stroke_opacity = clamp(decs['opacity'].to_f, 0, 1) if decs['opacity']
299
+ fill_opacity = clamp(decs['fill-opacity'].to_f, 0, 1) if decs['fill-opacity']
300
+ stroke_opacity = clamp(decs['stroke-opacity'].to_f, 0, 1) if decs['stroke-opacity']
297
301
 
298
- if fill_opacity || stroke_opacity
299
- state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1)
300
- state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1)
302
+ if fill_opacity || stroke_opacity
303
+ state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1)
304
+ state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1)
301
305
 
302
- calls << ['transparent', [state[:fill_opacity], state[:stroke_opacity]], []]
303
- calls = calls.last.last
304
- end
306
+ calls << ['transparent', [state[:fill_opacity], state[:stroke_opacity]], []]
307
+ calls = calls.last.last
308
+ end
305
309
 
306
- # Fill and stroke
307
- if decs['fill'] && decs['fill'] != "none"
308
- if color = color_to_hex(decs['fill'])
309
- calls << ['fill_color', [color], []]
310
- end
311
- draw_types << 'fill'
312
- end
310
+ # Fill and stroke
311
+ if decs['fill'] && decs['fill'] != "none"
312
+ if color = color_to_hex(decs['fill'])
313
+ calls << ['fill_color', [color], []]
314
+ end
315
+ draw_types << 'fill'
316
+ end
313
317
 
314
- if decs['stroke'] && decs['stroke'] != "none"
315
- if color = color_to_hex(decs['stroke'])
316
- calls << ['stroke_color', [color], []]
317
- end
318
- draw_types << 'stroke'
319
- end
318
+ if decs['stroke'] && decs['stroke'] != "none"
319
+ if color = color_to_hex(decs['stroke'])
320
+ calls << ['stroke_color', [color], []]
321
+ end
322
+ draw_types << 'stroke'
323
+ end
320
324
 
321
- calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width']
325
+ calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width']
322
326
 
323
- draw_type = draw_types.join("_and_")
324
- state[:draw_type] = draw_type if draw_type != ""
325
- if state[:draw_type] && !%w(g svg).include?(element.name)
326
- calls << [state[:draw_type], [], []]
327
- calls = calls.last.last
328
- end
327
+ draw_type = draw_types.join("_and_")
328
+ state[:draw_type] = draw_type if draw_type != ""
329
+ if state[:draw_type] && !%w(g svg).include?(element.name)
330
+ calls << [state[:draw_type], [], []]
331
+ calls = calls.last.last
332
+ end
329
333
 
330
- [calls, decs]
331
- end
334
+ [calls, decs]
335
+ end
332
336
 
333
- def parse_css_method_calls(string)
334
- string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
335
- name, argument_string = call
336
- arguments = argument_string.split(",").collect(&:strip)
337
- [name, arguments]
338
- end
339
- end
337
+ def parse_css_method_calls(string)
338
+ string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
339
+ name, argument_string = call
340
+ arguments = argument_string.split(",").collect(&:strip)
341
+ [name, arguments]
342
+ end
343
+ end
340
344
 
341
- BUILT_IN_FONTS = ["Courier", "Helvetica", "Times-Roman", "Symbol", "ZapfDingbats"]
342
- GENERIC_CSS_FONT_MAPPING = {
343
- "serif" => "Times-Roman",
344
- "sans-serif" => "Helvetica",
345
- "cursive" => "Times-Roman",
346
- "fantasy" => "Times-Roman",
347
- "monospace" => "Courier"}
345
+ BUILT_IN_FONTS = ["Courier", "Helvetica", "Times-Roman", "Symbol", "ZapfDingbats"]
346
+ GENERIC_CSS_FONT_MAPPING = {
347
+ "serif" => "Times-Roman",
348
+ "sans-serif" => "Helvetica",
349
+ "cursive" => "Times-Roman",
350
+ "fantasy" => "Times-Roman",
351
+ "monospace" => "Courier"}
348
352
 
349
- def installed_fonts
350
- @installed_fonts ||= Prawn::Svg.font_path.uniq.collect {|path| Dir["#{path}/*"]}.flatten
351
- end
353
+ def installed_fonts
354
+ @installed_fonts ||= Prawn::Svg::Interface.font_path.uniq.collect {|path| Dir["#{path}/*"]}.flatten
355
+ end
352
356
 
353
- def map_font_family_to_pdf_font(font_family)
354
- font_family.split(",").detect do |font|
355
- font = font.gsub(/['"]/, '').gsub(/\s{2,}/, ' ').strip.downcase
357
+ def map_font_family_to_pdf_font(font_family)
358
+ font_family.split(",").detect do |font|
359
+ font = font.gsub(/['"]/, '').gsub(/\s{2,}/, ' ').strip.downcase
356
360
 
357
- built_in_font = BUILT_IN_FONTS.detect {|f| f.downcase == font}
358
- break built_in_font if built_in_font
361
+ built_in_font = BUILT_IN_FONTS.detect {|f| f.downcase == font}
362
+ break built_in_font if built_in_font
359
363
 
360
- generic_font = GENERIC_CSS_FONT_MAPPING[font]
361
- break generic_font if generic_font
364
+ generic_font = GENERIC_CSS_FONT_MAPPING[font]
365
+ break generic_font if generic_font
362
366
 
363
- installed_font = installed_fonts.detect do |file|
364
- (matches = File.basename(file).match(/(.+)\./)) && matches[1].downcase == font
367
+ installed_font = installed_fonts.detect do |file|
368
+ (matches = File.basename(file).match(/(.+)\./)) && matches[1].downcase == font
369
+ end
370
+ break installed_font if installed_font
371
+ end
365
372
  end
366
- break installed_font if installed_font
367
- end
368
- end
369
373
 
370
- # TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords
371
- HTML_COLORS = {
372
- 'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00",
373
- 'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00",
374
- 'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff",
375
- 'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff"
376
- }.freeze
374
+ # TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords
375
+ HTML_COLORS = {
376
+ 'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00",
377
+ 'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00",
378
+ 'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff",
379
+ 'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff"
380
+ }.freeze
377
381
 
378
- RGB_VALUE_REGEXP = "\s*(-?[0-9.]+%?)\s*"
379
- RGB_REGEXP = /\Argb\(#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP}\)\z/i
382
+ RGB_VALUE_REGEXP = "\s*(-?[0-9.]+%?)\s*"
383
+ RGB_REGEXP = /\Argb\(#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP}\)\z/i
380
384
 
381
- def color_to_hex(color_string)
382
- color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_|
383
- if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i)
384
- break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}"
385
- elsif color.match(/\A#[0-9a-f]{6}\z/i)
386
- break color[1..6]
387
- elsif hex = HTML_COLORS[color.downcase]
388
- break hex
389
- elsif m = color.match(RGB_REGEXP)
390
- break (1..3).collect do |n|
391
- value = m[n].to_f
392
- value *= 2.55 if m[n][-1..-1] == '%'
393
- "%02x" % clamp(value.round, 0, 255)
394
- end.join
395
- end
396
- end
397
- end
385
+ def color_to_hex(color_string)
386
+ color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_|
387
+ if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i)
388
+ break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}"
389
+ elsif color.match(/\A#[0-9a-f]{6}\z/i)
390
+ break color[1..6]
391
+ elsif hex = HTML_COLORS[color.downcase]
392
+ break hex
393
+ elsif m = color.match(RGB_REGEXP)
394
+ break (1..3).collect do |n|
395
+ value = m[n].to_f
396
+ value *= 2.55 if m[n][-1..-1] == '%'
397
+ "%02x" % clamp(value.round, 0, 255)
398
+ end.join
399
+ end
400
+ end
401
+ end
398
402
 
399
- def x(value)
400
- (points(value, :x) - @x_offset) * scale
401
- end
403
+ def x(value)
404
+ (points(value, :x) - @x_offset) * scale
405
+ end
402
406
 
403
- def y(value)
404
- (@actual_height - (points(value, :y) - @y_offset)) * scale
405
- end
407
+ def y(value)
408
+ (@actual_height - (points(value, :y) - @y_offset)) * scale
409
+ end
406
410
 
407
- def distance(value, axis = nil)
408
- value && (points(value, axis) * scale)
409
- end
411
+ def distance(value, axis = nil)
412
+ value && (points(value, axis) * scale)
413
+ end
410
414
 
411
- def points(value, axis = nil)
412
- if value.is_a?(String)
413
- if match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/)
414
- send("#{match[1]}2pt", value.to_f)
415
- elsif value[-1..-1] == "%"
416
- value.to_f * (axis == :y ? @actual_height : @actual_width) / 100.0
417
- else
418
- value.to_f
415
+ def points(value, axis = nil)
416
+ if value.is_a?(String)
417
+ if match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/)
418
+ send("#{match[1]}2pt", value.to_f)
419
+ elsif value[-1..-1] == "%"
420
+ value.to_f * (axis == :y ? @actual_height : @actual_width) / 100.0
421
+ else
422
+ value.to_f
423
+ end
424
+ else
425
+ value.to_f
426
+ end
419
427
  end
420
- else
421
- value.to_f
422
- end
423
- end
424
428
 
425
- def calculate_dimensions
426
- if @options[:width]
427
- @width = @options[:width]
428
- @scale = @options[:width] / @actual_width.to_f
429
- elsif @options[:height]
430
- @height = @options[:height]
431
- @scale = @options[:height] / @actual_height.to_f
432
- else
433
- @scale = 1
434
- end
429
+ def calculate_dimensions
430
+ if @options[:width]
431
+ @width = @options[:width]
432
+ @scale = @options[:width] / @actual_width.to_f
433
+ elsif @options[:height]
434
+ @height = @options[:height]
435
+ @scale = @options[:height] / @actual_height.to_f
436
+ else
437
+ @scale = 1
438
+ end
435
439
 
436
- @width ||= @actual_width * @scale
437
- @height ||= @actual_height * @scale
438
- end
440
+ @width ||= @actual_width * @scale
441
+ @height ||= @actual_height * @scale
442
+ end
439
443
 
440
- def clamp(value, min_value, max_value)
441
- [[value, min_value].max, max_value].min
442
- end
444
+ def clamp(value, min_value, max_value)
445
+ [[value, min_value].max, max_value].min
446
+ end
443
447
 
444
- def check_attrs_present(element, attrs)
445
- missing_attrs = attrs - element.attributes.keys
446
- if missing_attrs.any?
447
- @warnings << "Must have attributes #{missing_attrs.join(", ")} on tag #{element.name}; skipping tag"
448
+ def check_attrs_present(element, attrs)
449
+ missing_attrs = attrs - element.attributes.keys
450
+ if missing_attrs.any?
451
+ @warnings << "Must have attributes #{missing_attrs.join(", ")} on tag #{element.name}; skipping tag"
452
+ end
453
+ missing_attrs.empty?
454
+ end
448
455
  end
449
- missing_attrs.empty?
450
456
  end
451
457
  end