prawn-svg 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +21 -0
- data/README +17 -0
- data/lib/prawn-svg.rb +3 -0
- data/lib/prawn/svg/svg.rb +320 -0
- data/lib/prawn/svg/svg_path.rb +167 -0
- data/lib/prawn/svg_document.rb +7 -0
- metadata +67 -0
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright 2010 Roger Nesbitt
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
The very start of an SVG renderer for Prawn.
|
2
|
+
|
3
|
+
This will take an SVG file as input and render it into your PDF. Find out more about the Prawn PDF library at:
|
4
|
+
|
5
|
+
http://wiki.github.com/sandal/prawn/
|
6
|
+
|
7
|
+
Using prawn-svg:
|
8
|
+
|
9
|
+
Prawn::Document.generate("svg.pdf") do
|
10
|
+
svg svg_data, :at => [x, y], :width => w
|
11
|
+
end
|
12
|
+
|
13
|
+
:at must be specified. :width, :height, or neither may be specified; if neither is present,
|
14
|
+
the resolution specified in the SVG will be used.
|
15
|
+
|
16
|
+
Note that only a very small subset of SVG is currently supported. It's just enough so that
|
17
|
+
it renders a simple graph made by Scruffy.
|
data/lib/prawn-svg.rb
ADDED
@@ -0,0 +1,320 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'prawn'
|
3
|
+
|
4
|
+
class Prawn::Svg
|
5
|
+
include Prawn::Measurements
|
6
|
+
|
7
|
+
attr_reader :data, :prawn, :options
|
8
|
+
attr_accessor :scale
|
9
|
+
|
10
|
+
def initialize(data, prawn, options)
|
11
|
+
@data = data
|
12
|
+
@prawn = prawn
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def draw
|
17
|
+
root = parse_document
|
18
|
+
calculate_dimensions
|
19
|
+
|
20
|
+
prawn.bounding_box(@options[:at], :width => @width, :height => @height) do
|
21
|
+
prawn.save_graphics_state do
|
22
|
+
call_tree = generate_call_tree(root)
|
23
|
+
proc_creator(prawn, call_tree).call
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate_call_tree(element)
|
29
|
+
[].tap {|calls| parse_element(element, calls, {})}
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
protected
|
34
|
+
def proc_creator(prawn, calls)
|
35
|
+
Proc.new {issue_prawn_command(prawn, calls)}
|
36
|
+
end
|
37
|
+
|
38
|
+
def issue_prawn_command(prawn, calls)
|
39
|
+
calls.each do |call, arguments, children|
|
40
|
+
if children.empty?
|
41
|
+
rewrite_call_arguments(prawn, call, arguments)
|
42
|
+
prawn.send(call, *arguments)
|
43
|
+
else
|
44
|
+
prawn.send(call, *arguments, &proc_creator(prawn, children))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def rewrite_call_arguments(prawn, call, arguments)
|
50
|
+
if call == 'text_box'
|
51
|
+
if (anchor = arguments.last.delete(:text_anchor)) && %w(middle end).include?(anchor)
|
52
|
+
width = prawn.width_of(*arguments)
|
53
|
+
width /= 2 if anchor == 'middle'
|
54
|
+
arguments.last[:at][0] -= width
|
55
|
+
end
|
56
|
+
|
57
|
+
arguments.last[:at][1] += prawn.height_of(*arguments) / 3 * 2
|
58
|
+
end
|
59
|
+
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
|
+
calls << ['line', [x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])], []]
|
139
|
+
|
140
|
+
when 'polyline'
|
141
|
+
points = attrs['points'].split(/\s+/)
|
142
|
+
x, y = points.shift.split(",")
|
143
|
+
calls << ['move_to', [x(x), y(y)], []]
|
144
|
+
calls << ['stroke', [], []]
|
145
|
+
calls = calls.last.last
|
146
|
+
points.each do |point|
|
147
|
+
x, y = point.split(",")
|
148
|
+
calls << ["line_to", [x(x), y(y)], []]
|
149
|
+
end
|
150
|
+
|
151
|
+
when 'polygon'
|
152
|
+
points = attrs['points'].split(/\s+/).collect do |point|
|
153
|
+
x, y = point.split(",")
|
154
|
+
[x(x), y(y)]
|
155
|
+
end
|
156
|
+
calls << ["polygon", points, []]
|
157
|
+
|
158
|
+
when 'circle'
|
159
|
+
calls << ["circle_at",
|
160
|
+
[[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}],
|
161
|
+
[]]
|
162
|
+
|
163
|
+
when 'ellipse'
|
164
|
+
calls << ["ellipse_at",
|
165
|
+
[[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])],
|
166
|
+
[]]
|
167
|
+
|
168
|
+
when 'rect'
|
169
|
+
radius = distance(attrs['rx'] || attrs['ry'])
|
170
|
+
args = [[x(attrs['x']), y(attrs['y'])], distance(attrs['width']), distance(attrs['height'])]
|
171
|
+
if radius
|
172
|
+
# n.b. does not support both rx and ry being specified with different values
|
173
|
+
calls << ["rounded_rectangle", args + [radius], []]
|
174
|
+
else
|
175
|
+
calls << ["rectangle", args, []]
|
176
|
+
end
|
177
|
+
|
178
|
+
when 'path'
|
179
|
+
@svg_path ||= Path.new
|
180
|
+
@svg_path.parse(attrs['d']).each do |command, args|
|
181
|
+
point_to = [x(args[0]), y(args[1])]
|
182
|
+
if command == 'curve_to'
|
183
|
+
bounds = [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]
|
184
|
+
calls << [command, [point_to, {:bounds => bounds}], []]
|
185
|
+
else
|
186
|
+
calls << [command, point_to, []]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
else
|
191
|
+
#raise "unknown tag #{element.name}"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def parse_css_declarations(declarations)
|
196
|
+
# copied from css_parser
|
197
|
+
declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')
|
198
|
+
|
199
|
+
{}.tap do |o|
|
200
|
+
declarations.split(/[\;$]+/m).each do |decs|
|
201
|
+
if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
|
202
|
+
property, value, end_of_declaration = matches.captures
|
203
|
+
o[property] = value
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def apply_styles(attrs, calls, state)
|
210
|
+
draw_types = []
|
211
|
+
|
212
|
+
decs = attrs["style"] ? parse_css_declarations(attrs["style"]) : {}
|
213
|
+
attrs.each {|n,v| decs[n] = v unless decs[n]}
|
214
|
+
|
215
|
+
# Opacity:
|
216
|
+
# We can't do nested opacities quite like the SVG requires, but this is close enough.
|
217
|
+
fill_opacity = stroke_opacity = clamp(decs['opacity'].to_f, 0, 1) if decs['opacity']
|
218
|
+
fill_opacity = clamp(decs['fill-opacity'].to_f, 0, 1) if decs['fill-opacity']
|
219
|
+
stroke_opacity = clamp(decs['stroke-opacity'].to_f, 0, 1) if decs['stroke-opacity']
|
220
|
+
|
221
|
+
if fill_opacity || stroke_opacity
|
222
|
+
state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1)
|
223
|
+
state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1)
|
224
|
+
|
225
|
+
calls << ['transparent', [state[:fill_opacity], state[:stroke_opacity]], []]
|
226
|
+
calls = calls.last.last
|
227
|
+
end
|
228
|
+
|
229
|
+
if decs['fill'] && decs['fill'] != "none"
|
230
|
+
if color = color_to_hex(decs['fill'])
|
231
|
+
calls << ['fill_color', [color], []]
|
232
|
+
end
|
233
|
+
draw_types << 'fill'
|
234
|
+
end
|
235
|
+
|
236
|
+
if decs['stroke'] && decs['stroke'] != "none"
|
237
|
+
if color = color_to_hex(decs['stroke'])
|
238
|
+
calls << ['stroke_color', [color], []]
|
239
|
+
end
|
240
|
+
draw_types << 'stroke'
|
241
|
+
end
|
242
|
+
|
243
|
+
calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width']
|
244
|
+
|
245
|
+
[calls, decs, draw_types.join("_and_")]
|
246
|
+
end
|
247
|
+
|
248
|
+
def parse_css_method_calls(string)
|
249
|
+
string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
|
250
|
+
name, argument_string = call
|
251
|
+
arguments = argument_string.split(",").collect(&:strip)
|
252
|
+
[name, arguments]
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords
|
257
|
+
HTML_COLORS = {
|
258
|
+
'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00",
|
259
|
+
'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00",
|
260
|
+
'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff",
|
261
|
+
'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff"
|
262
|
+
}.freeze
|
263
|
+
|
264
|
+
def color_to_hex(color_string)
|
265
|
+
color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_|
|
266
|
+
if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i)
|
267
|
+
break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}"
|
268
|
+
elsif color.match(/\A#[0-9a-f]{6}\z/i)
|
269
|
+
break color[1..6]
|
270
|
+
elsif hex = HTML_COLORS[color.downcase]
|
271
|
+
break hex
|
272
|
+
elsif m = color.match(/\Argb\(\s*(-?[0-9.]+%?)\s*,\s*(-?[0-9.]+%?)\s*,\s*(-?[0-9.]+%?)\s*\)\z/i)
|
273
|
+
break (1..3).collect do |n|
|
274
|
+
value = m[n].to_f
|
275
|
+
value *= 2.55 if m[n][-1..-1] == '%'
|
276
|
+
"%02x" % clamp(value.round, 0, 255)
|
277
|
+
end.join
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def x(value)
|
283
|
+
(pixels(value) - @x_offset) * scale
|
284
|
+
end
|
285
|
+
|
286
|
+
def y(value)
|
287
|
+
(@actual_height - (pixels(value) - @y_offset)) * scale
|
288
|
+
end
|
289
|
+
|
290
|
+
def distance(value)
|
291
|
+
value && (pixels(value) * scale)
|
292
|
+
end
|
293
|
+
|
294
|
+
def pixels(value)
|
295
|
+
if value.is_a?(String) && match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/)
|
296
|
+
send("#{match[1]}2pt", value.to_f)
|
297
|
+
else
|
298
|
+
value.to_f
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def calculate_dimensions
|
303
|
+
if @options[:width]
|
304
|
+
@width = @options[:width]
|
305
|
+
@scale = @options[:width] / @actual_width.to_f
|
306
|
+
elsif @options[:height]
|
307
|
+
@height = @options[:height]
|
308
|
+
@scale = @options[:height] / @actual_height.to_f
|
309
|
+
else
|
310
|
+
@scale = 1
|
311
|
+
end
|
312
|
+
|
313
|
+
@width ||= @actual_width * @scale
|
314
|
+
@height ||= @actual_height * @scale
|
315
|
+
end
|
316
|
+
|
317
|
+
def clamp(value, min_value, max_value)
|
318
|
+
[[value, min_value].max, max_value].min
|
319
|
+
end
|
320
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
class Prawn::Svg::Path
|
2
|
+
def parse(data)
|
3
|
+
cmd = values = value = nil
|
4
|
+
@subpath_initial_point = @last_point = nil
|
5
|
+
@previous_control_point = @previous_quadratic_control_point = nil
|
6
|
+
@calls = []
|
7
|
+
|
8
|
+
data.each_char do |c|
|
9
|
+
if c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'
|
10
|
+
run_path_command(cmd, values) if cmd
|
11
|
+
cmd = c
|
12
|
+
values = []
|
13
|
+
value = ""
|
14
|
+
elsif c >= '0' && c <= '9' || c == '.' || c == "-"
|
15
|
+
value << c
|
16
|
+
elsif c == ' ' || c == "\t" || c == "\r" || c == "\n" || c == ","
|
17
|
+
if value != ""
|
18
|
+
values << value.to_f
|
19
|
+
value = ""
|
20
|
+
end
|
21
|
+
else
|
22
|
+
raise "invalid character '#{c}' in path data"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
values << value.to_f if value != ""
|
27
|
+
run_path_command(cmd, values) if cmd
|
28
|
+
|
29
|
+
@calls
|
30
|
+
end
|
31
|
+
|
32
|
+
def run_path_command(command, values)
|
33
|
+
upcase_command = command.upcase
|
34
|
+
relative = command != upcase_command
|
35
|
+
|
36
|
+
case upcase_command
|
37
|
+
when 'M' # moveto
|
38
|
+
x = values.shift
|
39
|
+
y = values.shift
|
40
|
+
|
41
|
+
if relative && @last_point
|
42
|
+
x += @last_point.first
|
43
|
+
y += @last_point.last
|
44
|
+
end
|
45
|
+
|
46
|
+
@last_point = @subpath_initial_point = [x, y]
|
47
|
+
@calls << ["move_to", @last_point]
|
48
|
+
|
49
|
+
return run_path_command('L', values) if values.any?
|
50
|
+
|
51
|
+
when 'Z' # closepath
|
52
|
+
if @subpath_initial_point
|
53
|
+
@calls << ["line_to", @subpath_initial_point]
|
54
|
+
@last_point = @subpath_initial_point
|
55
|
+
end
|
56
|
+
|
57
|
+
when 'L' # lineto
|
58
|
+
while values.any?
|
59
|
+
x = values.shift
|
60
|
+
y = values.shift
|
61
|
+
if relative && @last_point
|
62
|
+
x += @last_point.first
|
63
|
+
y += @last_point.last
|
64
|
+
end
|
65
|
+
@last_point = [x, y]
|
66
|
+
@calls << ["line_to", @last_point]
|
67
|
+
end
|
68
|
+
|
69
|
+
when 'H' # horizontal lineto
|
70
|
+
while values.any?
|
71
|
+
x = values.shift
|
72
|
+
x += @last_point.first if relative && @last_point
|
73
|
+
@last_point = [x, @last_point.last]
|
74
|
+
@calls << ["line_to", @last_point]
|
75
|
+
end
|
76
|
+
|
77
|
+
when 'V' # vertical lineto
|
78
|
+
while values.any?
|
79
|
+
y = values.shift
|
80
|
+
y += @last_point.last if relative && @last_point
|
81
|
+
@last_point = [@last_point.first, y]
|
82
|
+
@calls << ["line_to", @last_point]
|
83
|
+
end
|
84
|
+
|
85
|
+
when 'C' # curveto
|
86
|
+
while values.any?
|
87
|
+
x1, y1, x2, y2, x, y = (1..6).collect {values.shift}
|
88
|
+
if relative && @last_point
|
89
|
+
x += @last_point.first
|
90
|
+
x1 += @last_point.first
|
91
|
+
x2 += @last_point.first
|
92
|
+
y += @last_point.last
|
93
|
+
y1 += @last_point.last
|
94
|
+
y2 += @last_point.last
|
95
|
+
end
|
96
|
+
|
97
|
+
@last_point = [x, y]
|
98
|
+
@previous_control_point = [x2, y2]
|
99
|
+
@calls << ["curve_to", [x, y, x1, y1, x2, y2]]
|
100
|
+
end
|
101
|
+
|
102
|
+
when 'S' # shorthand/smooth curveto
|
103
|
+
while values.any?
|
104
|
+
x2, y2, x, y = (1..4).collect {values.shift}
|
105
|
+
if relative && @last_point
|
106
|
+
x += @last_point.first
|
107
|
+
x2 += @last_point.first
|
108
|
+
y += @last_point.last
|
109
|
+
y2 += @last_point.last
|
110
|
+
end
|
111
|
+
|
112
|
+
if @previous_control_point
|
113
|
+
x1 = 2 * @last_point.first - @previous_control_point.first
|
114
|
+
y1 = 2 * @last_point.last - @previous_control_point.last
|
115
|
+
else
|
116
|
+
x1, y1 = @last_point
|
117
|
+
end
|
118
|
+
|
119
|
+
@last_point = [x, y]
|
120
|
+
@previous_control_point = [x2, y2]
|
121
|
+
@calls << ["curve_to", [x, y, x1, y1, x2, y2]]
|
122
|
+
end
|
123
|
+
|
124
|
+
when 'Q', 'T' # quadratic curveto
|
125
|
+
while values.any?
|
126
|
+
if shorthand = upcase_command == 'T'
|
127
|
+
x, y = (1..2).collect {values.shift}
|
128
|
+
else
|
129
|
+
x1, y1, x, y = (1..4).collect {values.shift}
|
130
|
+
end
|
131
|
+
|
132
|
+
if relative && @last_point
|
133
|
+
x += @last_point.first
|
134
|
+
x1 += @last_point.first if x1
|
135
|
+
y += @last_point.last
|
136
|
+
y1 += @last_point.last if y1
|
137
|
+
end
|
138
|
+
|
139
|
+
if shorthand
|
140
|
+
if @previous_quadratic_control_point
|
141
|
+
x1 = 2 * @last_point.first - @previous_quadratic_control_point.first
|
142
|
+
y1 = 2 * @last_point.last - @previous_quadratic_control_point.last
|
143
|
+
else
|
144
|
+
x1, y1 = @last_point
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# convert from quadratic to cubic
|
149
|
+
cx1 = @last_point.first + (x1 - @last_point.first) * 2 / 3.0
|
150
|
+
cy1 = @last_point.last + (y1 - @last_point.last) * 2 / 3.0
|
151
|
+
cx2 = cx1 + (x - @last_point.first) / 3.0
|
152
|
+
cy2 = cy1 + (y - @last_point.last) / 3.0
|
153
|
+
|
154
|
+
@last_point = [x, y]
|
155
|
+
@previous_quadratic_control_point = [x1, y1]
|
156
|
+
|
157
|
+
@calls << ["curve_to", [x, y, cx1, cy1, cx2, cy2]]
|
158
|
+
end
|
159
|
+
|
160
|
+
when 'A'
|
161
|
+
# unsupported
|
162
|
+
end
|
163
|
+
|
164
|
+
@previous_control_point = nil unless %w(C S).include?(upcase_command)
|
165
|
+
@previous_quadratic_control_point = nil unless %w(Q T).include?(upcase_command)
|
166
|
+
end
|
167
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: prawn-svg
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 9
|
8
|
+
- 1
|
9
|
+
version: 0.9.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Roger Nesbitt
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-26 00:00:00 +13:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: SVG renderer for Prawn PDF library
|
22
|
+
email: roger@seriousorange.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- README
|
31
|
+
- LICENSE
|
32
|
+
- lib/prawn/svg/svg.rb
|
33
|
+
- lib/prawn/svg/svg_path.rb
|
34
|
+
- lib/prawn/svg_document.rb
|
35
|
+
- lib/prawn-svg.rb
|
36
|
+
has_rdoc: true
|
37
|
+
homepage:
|
38
|
+
licenses: []
|
39
|
+
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
segments:
|
50
|
+
- 0
|
51
|
+
version: "0"
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.3.6
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: SVG renderer for Prawn PDF library
|
66
|
+
test_files: []
|
67
|
+
|