prawn-svg 0.9.1.2 → 0.9.1.3

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