prawn-svg 0.9.1.6 → 0.9.1.7

Sign up to get free protection for your applications and to get access to all the features.
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