prawn-svg 0.9.1.2 → 0.9.1.3

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/README CHANGED
@@ -38,4 +38,6 @@ prawn-svg is in its infancy and does not support the full SVG specifications. I
38
38
 
39
39
  - colors: html standard names, #xxx, #xxxxxx, rgb(1, 2, 3), rgb(1%, 2%, 3%)
40
40
 
41
+ - measurements specified in pt, cm, dm, ft, in, m, mm, yd
42
+
41
43
  prawn-svg does NOT support CSS classes, named elements, anything in the defs tag, the tspan tag, gradients/patterns or markers.
data/lib/prawn-svg.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'prawn'
1
2
  require 'prawn/svg_document'
2
3
  require 'prawn/svg/svg'
3
- require 'prawn/svg/svg_path'
4
+ require 'prawn/svg/parser'
5
+ require 'prawn/svg/path'
@@ -0,0 +1,352 @@
1
+ require 'rexml/document'
2
+
3
+ #
4
+ # Prawn::Svg::Parser is responsible for parsing an SVG file and converting it into a tree of
5
+ # prawn-compatible method calls.
6
+ #
7
+ # You probably do not want to use this class directly. Instead, use Prawn::Svg to draw
8
+ # SVG data to your Prawn::Document object.
9
+ #
10
+ # This class is not passed the prawn object, so knows nothing about
11
+ # prawn specifically - this might be useful if you want to take this code and use it to convert
12
+ # SVG to another format.
13
+ #
14
+ class Prawn::Svg::Parser
15
+ include Prawn::Measurements
16
+
17
+ attr_reader :width, :height
18
+
19
+ # An +Array+ of warnings that occurred while parsing the SVG data.
20
+ attr_reader :warnings
21
+
22
+ # The scaling factor, as determined by the :width or :height options.
23
+ attr_accessor :scale
24
+
25
+ #
26
+ # Construct a Parser object.
27
+ #
28
+ # The +data+ argument is SVG data. +options+ can optionally contain
29
+ # the key :width or :height. If both are specified, only :width will be used.
30
+ #
31
+ def initialize(data, options)
32
+ @data = data
33
+ @options = options
34
+ @warnings = []
35
+
36
+ if data
37
+ parse_document
38
+ calculate_dimensions
39
+ end
40
+ end
41
+
42
+ #
43
+ # Parse the SVG data and return a call tree. The returned +Array+ is in the format:
44
+ #
45
+ # [
46
+ # ['prawn_method_name', ['argument1', 'argument2'], []],
47
+ # ['method_that_takes_a_block', ['argument1', 'argument2'], [
48
+ # ['method_called_inside_block', ['argument'], []]
49
+ # ]
50
+ # ]
51
+ #
52
+ def parse
53
+ @warnings = []
54
+ [].tap {|calls| parse_element(@root, calls, {})}
55
+ end
56
+
57
+
58
+ private
59
+ def parse_document
60
+ @root = REXML::Document.new(@data).root
61
+
62
+ if vb = @root.attributes['viewBox']
63
+ x1, y1, x2, y2 = vb.strip.split(/\s+/)
64
+ @x_offset, @y_offset = [x1.to_f, y1.to_f]
65
+ @actual_width, @actual_height = [x2.to_f - x1.to_f, y2.to_f - y1.to_f]
66
+ else
67
+ @x_offset, @y_offset = [0, 0]
68
+ @actual_width = @root.attributes['width'].to_f
69
+ @actual_height = @root.attributes['height'].to_f
70
+ end
71
+ end
72
+
73
+ REQUIRED_ATTRIBUTES = {
74
+ "line" => %w(x1 y1 x2 y2),
75
+ "polyline" => %w(points),
76
+ "polygon" => %w(points),
77
+ "circle" => %w(r),
78
+ "ellipse" => %w(rx ry),
79
+ "rect" => %w(x y width height),
80
+ "path" => %w(d)
81
+ }
82
+
83
+ def parse_element(element, calls, state)
84
+ attrs = element.attributes
85
+
86
+ if transform = attrs['transform']
87
+ parse_css_method_calls(transform).each do |name, arguments|
88
+ case name
89
+ when 'translate'
90
+ x, y = arguments
91
+ x, y = x.split(/\s+/) if y.nil?
92
+ calls << [name, [distance(x), -distance(y)], []]
93
+ calls = calls.last.last
94
+ when 'rotate'
95
+ calls << [name, [-arguments.first.to_f, {:origin => [0, y('0')]}], []]
96
+ calls = calls.last.last
97
+ when 'scale'
98
+ calls << [name, [arguments.first.to_f], []]
99
+ calls = calls.last.last
100
+ else
101
+ @warnings << "Unknown transformation '#{name}'; ignoring"
102
+ end
103
+ end
104
+ end
105
+
106
+ calls, style_attrs, draw_type = apply_styles(attrs, calls, state)
107
+
108
+ state[:draw_type] = draw_type if draw_type != ""
109
+ if state[:draw_type] && !%w(g svg).include?(element.name)
110
+ calls << [state[:draw_type], [], []]
111
+ calls = calls.last.last
112
+ end
113
+
114
+ if required_attributes = REQUIRED_ATTRIBUTES[element.name]
115
+ return unless check_attrs_present(element, required_attributes)
116
+ end
117
+
118
+ case element.name
119
+ when 'defs', 'desc'
120
+ # ignore these tags
121
+
122
+ when 'g', 'svg'
123
+ element.elements.each do |child|
124
+ parse_element(child, calls, state.dup)
125
+ end
126
+
127
+ when 'text'
128
+ # very primitive support for fonts
129
+ if (font = style_attrs['font-family']) && !font.match(/[\/\\]/)
130
+ font = font.strip
131
+ if font != ""
132
+ calls << ['font', [font], []]
133
+ calls = calls.last.last
134
+ end
135
+ end
136
+
137
+ opts = {:at => [x(attrs['x']), y(attrs['y'])]}
138
+ if size = style_attrs['font-size']
139
+ opts[:size] = size.to_f * @scale
140
+ end
141
+
142
+ # This is not a prawn option but we can't work out how to render it here - it's handled by #rewrite_call
143
+ if anchor = style_attrs['text-anchor']
144
+ opts[:text_anchor] = anchor
145
+ end
146
+
147
+ calls << ['text_box', [element.text, opts], []]
148
+
149
+ when 'line'
150
+ calls << ['line', [x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])], []]
151
+
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
163
+
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, []]
170
+
171
+ when 'circle'
172
+ calls << ["circle_at",
173
+ [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}],
174
+ []]
175
+
176
+ when 'ellipse'
177
+ calls << ["ellipse_at",
178
+ [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])],
179
+ []]
180
+
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
190
+
191
+ when 'path'
192
+ @svg_path ||= Path.new
193
+
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
200
+
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, []]
208
+ end
209
+ end
210
+
211
+ else
212
+ @warnings << "Unknown tag '#{element.name}'; ignoring"
213
+ end
214
+ end
215
+
216
+ def parse_css_declarations(declarations)
217
+ # copied from css_parser
218
+ declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')
219
+
220
+ {}.tap do |o|
221
+ declarations.split(/[\;$]+/m).each do |decs|
222
+ if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
223
+ property, value, end_of_declaration = matches.captures
224
+ o[property] = value
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ def apply_styles(attrs, calls, state)
231
+ draw_types = []
232
+
233
+ decs = attrs["style"] ? parse_css_declarations(attrs["style"]) : {}
234
+ attrs.each {|n,v| decs[n] = v unless decs[n]}
235
+
236
+ # Opacity:
237
+ # We can't do nested opacities quite like the SVG requires, but this is close enough.
238
+ fill_opacity = stroke_opacity = clamp(decs['opacity'].to_f, 0, 1) if decs['opacity']
239
+ fill_opacity = clamp(decs['fill-opacity'].to_f, 0, 1) if decs['fill-opacity']
240
+ stroke_opacity = clamp(decs['stroke-opacity'].to_f, 0, 1) if decs['stroke-opacity']
241
+
242
+ if fill_opacity || stroke_opacity
243
+ state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1)
244
+ state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1)
245
+
246
+ calls << ['transparent', [state[:fill_opacity], state[:stroke_opacity]], []]
247
+ calls = calls.last.last
248
+ end
249
+
250
+ if decs['fill'] && decs['fill'] != "none"
251
+ if color = color_to_hex(decs['fill'])
252
+ calls << ['fill_color', [color], []]
253
+ end
254
+ draw_types << 'fill'
255
+ end
256
+
257
+ if decs['stroke'] && decs['stroke'] != "none"
258
+ if color = color_to_hex(decs['stroke'])
259
+ calls << ['stroke_color', [color], []]
260
+ end
261
+ draw_types << 'stroke'
262
+ end
263
+
264
+ calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width']
265
+
266
+ [calls, decs, draw_types.join("_and_")]
267
+ end
268
+
269
+ def parse_css_method_calls(string)
270
+ string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
271
+ name, argument_string = call
272
+ arguments = argument_string.split(",").collect(&:strip)
273
+ [name, arguments]
274
+ end
275
+ end
276
+
277
+ # TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords
278
+ HTML_COLORS = {
279
+ 'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00",
280
+ 'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00",
281
+ 'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff",
282
+ 'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff"
283
+ }.freeze
284
+
285
+ RGB_VALUE_REGEXP = "\s*(-?[0-9.]+%?)\s*"
286
+ RGB_REGEXP = /\Argb\(#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP}\)\z/i
287
+
288
+ def color_to_hex(color_string)
289
+ color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_|
290
+ if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i)
291
+ break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}"
292
+ elsif color.match(/\A#[0-9a-f]{6}\z/i)
293
+ break color[1..6]
294
+ elsif hex = HTML_COLORS[color.downcase]
295
+ break hex
296
+ elsif m = color.match(RGB_REGEXP)
297
+ break (1..3).collect do |n|
298
+ value = m[n].to_f
299
+ value *= 2.55 if m[n][-1..-1] == '%'
300
+ "%02x" % clamp(value.round, 0, 255)
301
+ end.join
302
+ end
303
+ end
304
+ end
305
+
306
+ def x(value)
307
+ (points(value) - @x_offset) * scale
308
+ end
309
+
310
+ def y(value)
311
+ (@actual_height - (points(value) - @y_offset)) * scale
312
+ end
313
+
314
+ def distance(value)
315
+ value && (points(value) * scale)
316
+ end
317
+
318
+ def points(value)
319
+ if value.is_a?(String) && match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/)
320
+ send("#{match[1]}2pt", value.to_f)
321
+ else
322
+ value.to_f
323
+ end
324
+ end
325
+
326
+ def calculate_dimensions
327
+ if @options[:width]
328
+ @width = @options[:width]
329
+ @scale = @options[:width] / @actual_width.to_f
330
+ elsif @options[:height]
331
+ @height = @options[:height]
332
+ @scale = @options[:height] / @actual_height.to_f
333
+ else
334
+ @scale = 1
335
+ end
336
+
337
+ @width ||= @actual_width * @scale
338
+ @height ||= @actual_height * @scale
339
+ end
340
+
341
+ def clamp(value, min_value, max_value)
342
+ [[value, min_value].max, max_value].min
343
+ end
344
+
345
+ def check_attrs_present(element, attrs)
346
+ missing_attrs = attrs - element.attributes.keys
347
+ if missing_attrs.any?
348
+ @warnings << "Must have attributes #{missing_attrs.join(", ")} on tag #{element.name}; skipping tag"
349
+ end
350
+ missing_attrs.empty?
351
+ end
352
+ end
@@ -1,17 +1,28 @@
1
- class Prawn::Svg::Path
1
+ class Prawn::Svg::Parser::Path
2
+ # Raised if the SVG path cannot be parsed.
3
+ InvalidError = Class.new(StandardError)
4
+
5
+ #
6
+ # Parses an SVG path and returns a Prawn-compatible call tree.
7
+ #
2
8
  def parse(data)
3
- cmd = values = value = nil
9
+ cmd = values = nil
10
+ value = ""
4
11
  @subpath_initial_point = @last_point = nil
5
12
  @previous_control_point = @previous_quadratic_control_point = nil
6
13
  @calls = []
7
14
 
8
15
  data.each_char do |c|
9
16
  if c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'
17
+ values << value.to_f if value != ""
10
18
  run_path_command(cmd, values) if cmd
11
19
  cmd = c
12
20
  values = []
13
21
  value = ""
14
22
  elsif c >= '0' && c <= '9' || c == '.' || c == "-"
23
+ unless cmd
24
+ raise InvalidError, "Numerical value specified before character command in SVG path data"
25
+ end
15
26
  value << c
16
27
  elsif c == ' ' || c == "\t" || c == "\r" || c == "\n" || c == ","
17
28
  if value != ""
@@ -19,7 +30,7 @@ class Prawn::Svg::Path
19
30
  value = ""
20
31
  end
21
32
  else
22
- raise "invalid character '#{c}' in path data"
33
+ raise InvalidError, "Invalid character '#{c}' in SVG path data"
23
34
  end
24
35
  end
25
36
 
@@ -29,6 +40,8 @@ class Prawn::Svg::Path
29
40
  @calls
30
41
  end
31
42
 
43
+
44
+ private
32
45
  def run_path_command(command, values)
33
46
  upcase_command = command.upcase
34
47
  relative = command != upcase_command
data/lib/prawn/svg/svg.rb CHANGED
@@ -1,36 +1,48 @@
1
- require 'rexml/document'
2
- require 'prawn'
3
-
1
+ #
2
+ # Prawn::Svg 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
+ #
4
5
  class Prawn::Svg
5
- include Prawn::Measurements
6
-
7
6
  attr_reader :data, :prawn, :options
8
- attr_accessor :scale
9
-
7
+
8
+ # An +Array+ of warnings that occurred while parsing the SVG data. If this array is non-empty,
9
+ # it's likely that the SVG failed to render correctly.
10
+ attr_reader :parser_warnings
11
+
12
+ #
13
+ # Creates a Prawn::Svg object.
14
+ #
15
+ # +data+ is the SVG data to convert. +prawn+ is your Prawn::Document object.
16
+ #
17
+ # +options+ must contain the key :at, which takes a tuple of x and y co-ordinates.
18
+ #
19
+ # +options+ can optionally contain the key :width or :height. If both are
20
+ # specified, only :width will be used.
21
+ #
10
22
  def initialize(data, prawn, options)
11
23
  @data = data
12
24
  @prawn = prawn
13
25
  @options = options
26
+
27
+ @options[:at] or raise "options[:at] must be specified"
28
+
29
+ @parser = Parser.new(data, options)
30
+ @parser_warnings = @parser.warnings
14
31
  end
15
32
 
33
+ #
34
+ # Draws the SVG to the Prawn::Document object.
35
+ #
16
36
  def draw
17
- root = parse_document
18
- calculate_dimensions
19
-
20
- prawn.bounding_box(@options[:at], :width => @width, :height => @height) do
37
+ prawn.bounding_box(@options[:at], :width => @parser.width, :height => @parser.height) do
21
38
  prawn.save_graphics_state do
22
- call_tree = generate_call_tree(root)
23
- proc_creator(prawn, call_tree).call
39
+ proc_creator(prawn, @parser.parse).call
24
40
  end
25
41
  end
26
42
  end
43
+
27
44
 
28
- def generate_call_tree(element)
29
- [].tap {|calls| parse_element(element, calls, {})}
30
- end
31
-
32
-
33
- protected
45
+ private
34
46
  def proc_creator(prawn, calls)
35
47
  Proc.new {issue_prawn_command(prawn, calls)}
36
48
  end
@@ -57,271 +69,4 @@ class Prawn::Svg
57
69
  arguments.last[:at][1] += prawn.height_of(*arguments) / 3 * 2
58
70
  end
59
71
  end
60
-
61
- def parse_document
62
- REXML::Document.new(@data).root.tap do |root|
63
- if vb = root.attributes['viewBox']
64
- x1, y1, x2, y2 = vb.strip.split(/\s+/)
65
- @x_offset, @y_offset = [x1.to_f, y1.to_f]
66
- @actual_width, @actual_height = [x2.to_f - x1.to_f, y2.to_f - y1.to_f]
67
- else
68
- @x_offset, @y_offset = [0, 0]
69
- @actual_width = root.attributes['width'].to_f
70
- @actual_height = root.attributes['height'].to_f
71
- end
72
- end
73
- end
74
-
75
- def parse_element(element, calls, state)
76
- attrs = element.attributes
77
-
78
- if transform = attrs['transform']
79
- parse_css_method_calls(transform).each do |name, arguments|
80
- case name
81
- when 'translate'
82
- x, y = arguments
83
- x, y = x.split(/\s+/) if y.nil?
84
- calls << [name, [distance(x), -distance(y)], []]
85
- calls = calls.last.last
86
- when 'rotate'
87
- calls << [name, [-arguments.first.to_f, {:origin => [0, y('0')]}], []]
88
- calls = calls.last.last
89
- when 'scale'
90
- calls << [name, [arguments.first.to_f], []]
91
- calls = calls.last.last
92
- else
93
- #raise "unknown transformation '#{name}'"
94
- end
95
- end
96
- end
97
-
98
- calls, style_attrs, draw_type = apply_styles(attrs, calls, state)
99
-
100
- state[:draw_type] = draw_type if draw_type != ""
101
- if state[:draw_type] && !%w(g svg).include?(element.name)
102
- calls << [state[:draw_type], [], []]
103
- calls = calls.last.last
104
- end
105
-
106
- case element.name
107
- when 'defs', 'desc'
108
- # ignore these tags
109
-
110
- when 'g', 'svg'
111
- element.elements.each do |child|
112
- parse_element(child, calls, state.dup)
113
- end
114
-
115
- when 'text'
116
- # very primitive support for fonts
117
- if (font = style_attrs['font-family']) && !font.match(/[\/\\]/)
118
- font = font.strip
119
- if font != ""
120
- calls << ['font', [font], []]
121
- calls = calls.last.last
122
- end
123
- end
124
-
125
- opts = {:at => [x(attrs['x']), y(attrs['y'])]}
126
- if size = style_attrs['font-size']
127
- opts[:size] = size.to_f * @scale
128
- end
129
-
130
- # This is not a prawn option but we can't work out how to render it here - it's handled by #rewrite_call
131
- if anchor = style_attrs['text-anchor']
132
- opts[:text_anchor] = anchor
133
- end
134
-
135
- calls << ['text_box', [element.text, opts], []]
136
-
137
- when 'line'
138
- return unless attrs['x1'] && attrs['y1'] && attrs['x2'] && attrs['y2']
139
- calls << ['line', [x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])], []]
140
-
141
- when 'polyline'
142
- return unless attrs['points']
143
- points = attrs['points'].split(/\s+/)
144
- return unless base_point = points.shift
145
- x, y = base_point.split(",")
146
- calls << ['move_to', [x(x), y(y)], []]
147
- calls << ['stroke', [], []]
148
- calls = calls.last.last
149
- points.each do |point|
150
- x, y = point.split(",")
151
- calls << ["line_to", [x(x), y(y)], []]
152
- end
153
-
154
- when 'polygon'
155
- return unless attrs['points']
156
- points = attrs['points'].split(/\s+/).collect do |point|
157
- x, y = point.split(",")
158
- [x(x), y(y)]
159
- end
160
- calls << ["polygon", points, []]
161
-
162
- when 'circle'
163
- return unless attrs['r']
164
- calls << ["circle_at",
165
- [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}],
166
- []]
167
-
168
- when 'ellipse'
169
- return unless attrs['rx'] && attrs['ry']
170
- calls << ["ellipse_at",
171
- [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])],
172
- []]
173
-
174
- when 'rect'
175
- return unless attrs['x'] && attrs['y'] && attrs['width'] && attrs['height']
176
- radius = distance(attrs['rx'] || attrs['ry'])
177
- args = [[x(attrs['x']), y(attrs['y'])], distance(attrs['width']), distance(attrs['height'])]
178
- if radius
179
- # n.b. does not support both rx and ry being specified with different values
180
- calls << ["rounded_rectangle", args + [radius], []]
181
- else
182
- calls << ["rectangle", args, []]
183
- end
184
-
185
- when 'path'
186
- @svg_path ||= Path.new
187
- @svg_path.parse(attrs['d']).each do |command, args|
188
- point_to = [x(args[0]), y(args[1])]
189
- if command == 'curve_to'
190
- bounds = [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]
191
- calls << [command, [point_to, {:bounds => bounds}], []]
192
- else
193
- calls << [command, point_to, []]
194
- end
195
- end
196
-
197
- else
198
- #raise "unknown tag #{element.name}"
199
- end
200
- end
201
-
202
- def parse_css_declarations(declarations)
203
- # copied from css_parser
204
- declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')
205
-
206
- {}.tap do |o|
207
- declarations.split(/[\;$]+/m).each do |decs|
208
- if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
209
- property, value, end_of_declaration = matches.captures
210
- o[property] = value
211
- end
212
- end
213
- end
214
- end
215
-
216
- def apply_styles(attrs, calls, state)
217
- draw_types = []
218
-
219
- decs = attrs["style"] ? parse_css_declarations(attrs["style"]) : {}
220
- attrs.each {|n,v| decs[n] = v unless decs[n]}
221
-
222
- # Opacity:
223
- # We can't do nested opacities quite like the SVG requires, but this is close enough.
224
- fill_opacity = stroke_opacity = clamp(decs['opacity'].to_f, 0, 1) if decs['opacity']
225
- fill_opacity = clamp(decs['fill-opacity'].to_f, 0, 1) if decs['fill-opacity']
226
- stroke_opacity = clamp(decs['stroke-opacity'].to_f, 0, 1) if decs['stroke-opacity']
227
-
228
- if fill_opacity || stroke_opacity
229
- state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1)
230
- state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1)
231
-
232
- calls << ['transparent', [state[:fill_opacity], state[:stroke_opacity]], []]
233
- calls = calls.last.last
234
- end
235
-
236
- if decs['fill'] && decs['fill'] != "none"
237
- if color = color_to_hex(decs['fill'])
238
- calls << ['fill_color', [color], []]
239
- end
240
- draw_types << 'fill'
241
- end
242
-
243
- if decs['stroke'] && decs['stroke'] != "none"
244
- if color = color_to_hex(decs['stroke'])
245
- calls << ['stroke_color', [color], []]
246
- end
247
- draw_types << 'stroke'
248
- end
249
-
250
- calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width']
251
-
252
- [calls, decs, draw_types.join("_and_")]
253
- end
254
-
255
- def parse_css_method_calls(string)
256
- string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
257
- name, argument_string = call
258
- arguments = argument_string.split(",").collect(&:strip)
259
- [name, arguments]
260
- end
261
- end
262
-
263
- # TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords
264
- HTML_COLORS = {
265
- 'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00",
266
- 'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00",
267
- 'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff",
268
- 'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff"
269
- }.freeze
270
-
271
- def color_to_hex(color_string)
272
- color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_|
273
- if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i)
274
- break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}"
275
- elsif color.match(/\A#[0-9a-f]{6}\z/i)
276
- break color[1..6]
277
- elsif hex = HTML_COLORS[color.downcase]
278
- break hex
279
- elsif m = color.match(/\Argb\(\s*(-?[0-9.]+%?)\s*,\s*(-?[0-9.]+%?)\s*,\s*(-?[0-9.]+%?)\s*\)\z/i)
280
- break (1..3).collect do |n|
281
- value = m[n].to_f
282
- value *= 2.55 if m[n][-1..-1] == '%'
283
- "%02x" % clamp(value.round, 0, 255)
284
- end.join
285
- end
286
- end
287
- end
288
-
289
- def x(value)
290
- (points(value) - @x_offset) * scale
291
- end
292
-
293
- def y(value)
294
- (@actual_height - (points(value) - @y_offset)) * scale
295
- end
296
-
297
- def distance(value)
298
- value && (points(value) * scale)
299
- end
300
-
301
- def points(value)
302
- if value.is_a?(String) && match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/)
303
- send("#{match[1]}2pt", value.to_f)
304
- else
305
- value.to_f
306
- end
307
- end
308
-
309
- def calculate_dimensions
310
- if @options[:width]
311
- @width = @options[:width]
312
- @scale = @options[:width] / @actual_width.to_f
313
- elsif @options[:height]
314
- @height = @options[:height]
315
- @scale = @options[:height] / @actual_height.to_f
316
- else
317
- @scale = 1
318
- end
319
-
320
- @width ||= @actual_width * @scale
321
- @height ||= @actual_height * @scale
322
- end
323
-
324
- def clamp(value, min_value, max_value)
325
- [[value, min_value].max, max_value].min
326
- end
327
72
  end
@@ -1,7 +1,22 @@
1
1
  module Prawn
2
2
  class Document
3
+ #
4
+ # Draws an SVG document into the PDF.
5
+ #
6
+ # +options+ must contain the key :at, which takes a tuple of x and y co-ordinates.
7
+ #
8
+ # +options+ can optionally contain the key :width or :height. If both are
9
+ # specified, only :width will be used. If neither are specified, the resolution
10
+ # given in the SVG will be used.
11
+ #
12
+ # Example usage:
13
+ #
14
+ # svg IO.read("example.svg"), :at => [100, 300], :width => 600
15
+ #
3
16
  def svg(data, options={})
4
- Prawn::Svg.new(data, self, options).draw
17
+ svg = Prawn::Svg.new(data, self, options)
18
+ svg.draw
19
+ {:warnings => svg.parser_warnings}
5
20
  end
6
21
  end
7
22
  end
metadata CHANGED
@@ -6,8 +6,8 @@ version: !ruby/object:Gem::Version
6
6
  - 0
7
7
  - 9
8
8
  - 1
9
- - 2
10
- version: 0.9.1.2
9
+ - 3
10
+ version: 0.9.1.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - Roger Nesbitt
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-03-26 00:00:00 +13:00
18
+ date: 2010-03-27 00:00:00 +13:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -57,8 +57,9 @@ extra_rdoc_files: []
57
57
  files:
58
58
  - README
59
59
  - LICENSE
60
+ - lib/prawn/svg/parser.rb
61
+ - lib/prawn/svg/path.rb
60
62
  - lib/prawn/svg/svg.rb
61
- - lib/prawn/svg/svg_path.rb
62
63
  - lib/prawn/svg_document.rb
63
64
  - lib/prawn-svg.rb
64
65
  has_rdoc: true