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 +2 -0
- data/lib/prawn-svg.rb +3 -1
- data/lib/prawn/svg/parser.rb +352 -0
- data/lib/prawn/svg/{svg_path.rb → path.rb} +16 -3
- data/lib/prawn/svg/svg.rb +31 -286
- data/lib/prawn/svg_document.rb +16 -1
- metadata +5 -4
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
@@ -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 =
|
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 "
|
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
|
-
|
2
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/prawn/svg_document.rb
CHANGED
@@ -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)
|
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
|
-
-
|
10
|
-
version: 0.9.1.
|
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-
|
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
|