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 +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
|