prawn-svg 0.15.0.0 → 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +20 -12
- data/lib/prawn/svg/calculators/aspect_ratio.rb +58 -0
- data/lib/prawn/svg/calculators/document_sizing.rb +75 -0
- data/lib/prawn/svg/calculators/pixels.rb +21 -0
- data/lib/prawn/svg/document.rb +16 -43
- data/lib/prawn/svg/element.rb +71 -18
- data/lib/prawn/svg/extension.rb +3 -3
- data/lib/prawn/svg/interface.rb +80 -19
- data/lib/prawn/svg/parser/image.rb +9 -62
- data/lib/prawn/svg/parser/path.rb +19 -1
- data/lib/prawn/svg/parser/text.rb +2 -0
- data/lib/prawn/svg/parser.rb +53 -18
- data/lib/prawn/svg/url_loader.rb +34 -0
- data/lib/prawn/svg/version.rb +1 -1
- data/lib/prawn-svg.rb +4 -0
- data/prawn-svg.gemspec +7 -4
- data/spec/integration_spec.rb +83 -0
- data/spec/prawn/svg/calculators/aspect_ratio_spec.rb +95 -0
- data/spec/prawn/svg/calculators/document_sizing_spec.rb +73 -0
- data/spec/prawn/svg/document_spec.rb +21 -17
- data/spec/prawn/svg/element_spec.rb +1 -1
- data/spec/prawn/svg/interface_spec.rb +59 -0
- data/spec/prawn/svg/parser/path_spec.rb +89 -0
- data/spec/prawn/svg/url_loader_spec.rb +46 -0
- data/spec/sample_images/mushroom-long.jpg +0 -0
- data/spec/sample_images/mushroom-wide.jpg +0 -0
- data/spec/sample_svg/cap_styles.svg +13 -0
- data/spec/sample_svg/display_none.svg +13 -0
- data/spec/sample_svg/gistfile1.svg +36 -0
- data/spec/sample_svg/hidden_paths.svg +6 -0
- data/spec/sample_svg/image01.svg +31 -31
- data/spec/sample_svg/negminy.svg +25 -0
- data/spec/sample_svg/path.svg +5 -0
- data/spec/sample_svg/pie_piece.svg +7 -0
- data/spec/sample_svg/viewbox.svg +4 -0
- data/spec/sample_svg/viewport.svg +23 -0
- data/spec/spec_helper.rb +7 -0
- metadata +81 -25
- data/spec/lib/path_spec.rb +0 -54
- data/spec/lib/svg_spec.rb +0 -47
- /data/spec/{lib → prawn/svg}/parser_spec.rb +0 -0
data/lib/prawn/svg/interface.rb
CHANGED
@@ -5,6 +5,8 @@
|
|
5
5
|
module Prawn
|
6
6
|
module Svg
|
7
7
|
class Interface
|
8
|
+
VALID_OPTIONS = [:at, :position, :vposition, :width, :height, :cache_images, :fallback_font_name]
|
9
|
+
|
8
10
|
DEFAULT_FONT_PATHS = ["/Library/Fonts", "/System/Library/Fonts", "#{ENV["HOME"]}/Library/Fonts", "/usr/share/fonts/truetype"]
|
9
11
|
|
10
12
|
@font_path = []
|
@@ -19,51 +21,99 @@ module Prawn
|
|
19
21
|
#
|
20
22
|
# +data+ is the SVG data to convert. +prawn+ is your Prawn::Document object.
|
21
23
|
#
|
22
|
-
#
|
24
|
+
# Options:
|
25
|
+
# <tt>:at</tt>:: an array [x,y] specifying the location of the top-left corner of the SVG.
|
26
|
+
# <tt>:position</tt>:: one of (nil, :left, :center, :right) or an x-offset
|
27
|
+
# <tt>:vposition</tt>:: one of (nil, :top, :center, :bottom) or a y-offset
|
28
|
+
# <tt>:width</tt>:: the width that the SVG is to be rendered
|
29
|
+
# <tt>:height</tt>:: the height that the SVG is to be rendered
|
30
|
+
#
|
31
|
+
# If <tt>:at</tt> is provided, the SVG will be placed in the current page but
|
32
|
+
# the text position will not be changed.
|
23
33
|
#
|
24
|
-
#
|
25
|
-
# specified, only :width will be used.
|
34
|
+
# If both <tt>:width</tt> and <tt>:height</tt> are specified, only the width will be used.
|
26
35
|
#
|
27
|
-
def initialize(data, prawn, options)
|
36
|
+
def initialize(data, prawn, options, &block)
|
37
|
+
Prawn.verify_options VALID_OPTIONS, options
|
38
|
+
|
28
39
|
@data = data
|
29
40
|
@prawn = prawn
|
30
41
|
@options = options
|
31
42
|
|
32
|
-
@options[:at] or raise "options[:at] must be specified"
|
33
|
-
|
34
43
|
Prawn::Svg::Font.load_external_fonts(prawn.font_families)
|
35
44
|
|
36
|
-
@document = Document.new(data, [prawn.bounds.width, prawn.bounds.height], options)
|
45
|
+
@document = Document.new(data, [prawn.bounds.width, prawn.bounds.height], options, &block)
|
37
46
|
end
|
38
47
|
|
39
48
|
#
|
40
49
|
# Draws the SVG to the Prawn::Document object.
|
41
50
|
#
|
42
51
|
def draw
|
43
|
-
prawn.bounding_box(
|
52
|
+
prawn.bounding_box(position, :width => @document.sizing.output_width, :height => @document.sizing.output_height) do
|
44
53
|
prawn.save_graphics_state do
|
45
|
-
clip_rectangle 0, 0, @document.
|
54
|
+
clip_rectangle 0, 0, @document.sizing.output_width, @document.sizing.output_height
|
46
55
|
proc_creator(prawn, Parser.new(@document).parse).call
|
47
56
|
end
|
48
57
|
end
|
49
58
|
end
|
50
59
|
|
60
|
+
def position
|
61
|
+
@options[:at] || [x_based_on_requested_alignment, y_based_on_requested_alignment]
|
62
|
+
end
|
51
63
|
|
52
64
|
private
|
65
|
+
|
66
|
+
def x_based_on_requested_alignment
|
67
|
+
case options[:position]
|
68
|
+
when :left, nil
|
69
|
+
0
|
70
|
+
when :center, :centre
|
71
|
+
(@document.sizing.bounds[0] - @document.sizing.output_width) / 2.0
|
72
|
+
when :right
|
73
|
+
@document.sizing.bounds[0] - @document.sizing.output_width
|
74
|
+
when Numeric
|
75
|
+
options[:position]
|
76
|
+
else
|
77
|
+
raise ArgumentError, "options[:position] must be one of nil, :left, :right, :center or a number"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def y_based_on_requested_alignment
|
82
|
+
case options[:vposition]
|
83
|
+
when nil
|
84
|
+
prawn.cursor
|
85
|
+
when :top
|
86
|
+
@document.sizing.bounds[1]
|
87
|
+
when :center, :centre
|
88
|
+
@document.sizing.bounds[1] - (@document.sizing.bounds[1] - @document.sizing.output_height) / 2.0
|
89
|
+
when :bottom
|
90
|
+
@document.sizing.output_height
|
91
|
+
when Numeric
|
92
|
+
@document.sizing.bounds[1] - options[:vposition]
|
93
|
+
else
|
94
|
+
raise ArgumentError, "options[:vposition] must be one of nil, :top, :right, :bottom or a number"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
53
98
|
def proc_creator(prawn, calls)
|
54
99
|
Proc.new {issue_prawn_command(prawn, calls)}
|
55
100
|
end
|
56
101
|
|
57
102
|
def issue_prawn_command(prawn, calls)
|
58
103
|
calls.each do |call, arguments, children|
|
59
|
-
|
104
|
+
skip = false
|
105
|
+
|
106
|
+
rewrite_call_arguments(prawn, call, arguments) do
|
60
107
|
issue_prawn_command(prawn, children) if children.any?
|
108
|
+
skip = true
|
109
|
+
end
|
110
|
+
|
111
|
+
if skip
|
112
|
+
# the call has been overridden
|
113
|
+
elsif children.empty?
|
114
|
+
prawn.send(call, *arguments)
|
61
115
|
else
|
62
|
-
|
63
|
-
prawn.send(call, *arguments)
|
64
|
-
else
|
65
|
-
prawn.send(call, *arguments, &proc_creator(prawn, children))
|
66
|
-
end
|
116
|
+
prawn.send(call, *arguments, &proc_creator(prawn, children))
|
67
117
|
end
|
68
118
|
end
|
69
119
|
end
|
@@ -77,7 +127,7 @@ module Prawn
|
|
77
127
|
case call
|
78
128
|
when 'text_group'
|
79
129
|
@relative_text_position = nil
|
80
|
-
|
130
|
+
yield
|
81
131
|
|
82
132
|
when 'draw_text'
|
83
133
|
text, options = arguments
|
@@ -100,15 +150,26 @@ module Prawn
|
|
100
150
|
|
101
151
|
when 'clip'
|
102
152
|
prawn.add_content "W n" # clip to path
|
103
|
-
|
153
|
+
yield
|
104
154
|
|
105
155
|
when 'save'
|
106
156
|
prawn.save_graphics_state
|
107
|
-
|
157
|
+
yield
|
108
158
|
|
109
159
|
when 'restore'
|
110
160
|
prawn.restore_graphics_state
|
111
|
-
|
161
|
+
yield
|
162
|
+
|
163
|
+
when "end_path"
|
164
|
+
yield
|
165
|
+
prawn.add_content "n" # end path
|
166
|
+
|
167
|
+
when 'fill_and_stroke'
|
168
|
+
yield
|
169
|
+
# prawn (as at 2.0.1 anyway) uses 'b' for its fill_and_stroke. 'b' is 'h' (closepath) + 'B', and we
|
170
|
+
# never want closepath to be automatically run as it stuffs up many drawing operations, such as dashes
|
171
|
+
# and line caps, and makes paths close that we didn't ask to be closed when fill is specified.
|
172
|
+
prawn.add_content 'B'
|
112
173
|
end
|
113
174
|
end
|
114
175
|
|
@@ -1,12 +1,6 @@
|
|
1
|
-
require 'open-uri'
|
2
|
-
require 'base64'
|
3
|
-
|
4
1
|
class Prawn::Svg::Parser::Image
|
5
2
|
Error = Class.new(StandardError)
|
6
3
|
|
7
|
-
DATAURL_REGEXP = /(data:image\/(png|jpg);base64(;[a-z0-9]+)*,)/
|
8
|
-
URL_REGEXP = /^https?:\/\/|#{DATAURL_REGEXP}/
|
9
|
-
|
10
4
|
class FakeIO
|
11
5
|
def initialize(data)
|
12
6
|
@data = data
|
@@ -24,18 +18,20 @@ class Prawn::Svg::Parser::Image
|
|
24
18
|
end
|
25
19
|
|
26
20
|
def parse(element)
|
21
|
+
return if element.state[:display] == "none"
|
22
|
+
|
27
23
|
attrs = element.attributes
|
28
24
|
url = attrs['xlink:href'] || attrs['href']
|
29
25
|
if url.nil?
|
30
26
|
raise Error, "image tag must have an xlink:href"
|
31
27
|
end
|
32
28
|
|
33
|
-
if
|
29
|
+
if !@document.url_loader.valid?(url)
|
34
30
|
raise Error, "image tag xlink:href attribute must use http, https or data scheme"
|
35
31
|
end
|
36
32
|
|
37
33
|
image = begin
|
38
|
-
|
34
|
+
@document.url_loader.load(url)
|
39
35
|
rescue => e
|
40
36
|
raise Error, "Error retrieving URL #{url}: #{e.message}"
|
41
37
|
end
|
@@ -48,50 +44,18 @@ class Prawn::Svg::Parser::Image
|
|
48
44
|
return if width.zero? || height.zero?
|
49
45
|
raise Error, "width and height must be 0 or higher" if width < 0 || height < 0
|
50
46
|
|
51
|
-
|
52
|
-
par.shift if par.first == "defer"
|
53
|
-
align, meet_or_slice = par
|
54
|
-
slice = meet_or_slice == "slice"
|
47
|
+
aspect = Prawn::Svg::Calculators::AspectRatio.new(attrs['preserveAspectRatio'], [width, height], image_dimensions(image))
|
55
48
|
|
56
|
-
if slice
|
49
|
+
if aspect.slice?
|
57
50
|
element.add_call "save"
|
58
51
|
element.add_call "rectangle", [x, y], width, height
|
59
52
|
element.add_call "clip"
|
60
53
|
end
|
61
54
|
|
62
|
-
options = {}
|
63
|
-
case align
|
64
|
-
when /\Ax(Min|Mid|Max)Y(Min|Mid|Max)\z/
|
65
|
-
ratio = image_ratio(image)
|
66
|
-
|
67
|
-
options[:fit] = [width, height] unless slice
|
68
|
-
|
69
|
-
if (width/height > ratio) == slice
|
70
|
-
options[:width] = width if slice
|
71
|
-
y -= case $2
|
72
|
-
when "Min" then 0
|
73
|
-
when "Mid" then (height - width/ratio)/2
|
74
|
-
when "Max" then height - width/ratio
|
75
|
-
end
|
76
|
-
else
|
77
|
-
options[:height] = height if slice
|
78
|
-
x += case $1
|
79
|
-
when "Min" then 0
|
80
|
-
when "Mid" then (width - height*ratio)/2
|
81
|
-
when "Max" then width - height*ratio
|
82
|
-
end
|
83
|
-
end
|
84
|
-
when 'none'
|
85
|
-
options[:width] = width
|
86
|
-
options[:height] = height
|
87
|
-
else
|
88
|
-
raise Error, "unknown preserveAspectRatio align keyword; ignoring image"
|
89
|
-
end
|
90
|
-
|
91
|
-
options[:at] = [x, y]
|
55
|
+
options = {:width => aspect.width, :height => aspect.height, :at => [x + aspect.x, y - aspect.y]}
|
92
56
|
|
93
57
|
element.add_call "image", FakeIO.new(image), options
|
94
|
-
element.add_call "restore" if slice
|
58
|
+
element.add_call "restore" if aspect.slice?
|
95
59
|
rescue Error => e
|
96
60
|
@document.warnings << e.message
|
97
61
|
end
|
@@ -108,24 +72,7 @@ class Prawn::Svg::Parser::Image
|
|
108
72
|
end
|
109
73
|
|
110
74
|
image = handler.new(data)
|
111
|
-
[image.width, image.height]
|
112
|
-
end
|
113
|
-
|
114
|
-
def image_ratio(data)
|
115
|
-
w, h = image_dimensions(data)
|
116
|
-
w.to_f / h.to_f
|
117
|
-
end
|
118
|
-
|
119
|
-
def retrieve_data_from_url(url)
|
120
|
-
@url_cache[url] || begin
|
121
|
-
if m = url.match(DATAURL_REGEXP)
|
122
|
-
data = Base64.decode64(url[m[0].length .. -1])
|
123
|
-
else
|
124
|
-
data = open(url).read
|
125
|
-
end
|
126
|
-
@url_cache[url] = data if @document.cache_images
|
127
|
-
data
|
128
|
-
end
|
75
|
+
[image.width.to_f, image.height.to_f]
|
129
76
|
end
|
130
77
|
|
131
78
|
%w(x y distance).each do |method|
|
@@ -10,6 +10,7 @@ module Prawn
|
|
10
10
|
VALUES_REGEXP = /^#{INSIDE_REGEXP}/
|
11
11
|
COMMAND_REGEXP = /^#{OUTSIDE_SPACE_REGEXP}([A-Za-z])((?:#{INSIDE_REGEXP})*)#{OUTSIDE_SPACE_REGEXP}/
|
12
12
|
|
13
|
+
FLOAT_ERROR_DELTA = 1e-10
|
13
14
|
|
14
15
|
#
|
15
16
|
# Parses an SVG path and returns a Prawn-compatible call tree.
|
@@ -173,15 +174,28 @@ module Prawn
|
|
173
174
|
rx, ry, phi, fa, fs, x2, y2 = (1..7).collect {values.shift}
|
174
175
|
x1, y1 = @last_point
|
175
176
|
|
177
|
+
return if rx.zero? && ry.zero?
|
178
|
+
|
176
179
|
if relative
|
177
180
|
x2 += x1
|
178
181
|
y2 += y1
|
179
182
|
end
|
180
183
|
|
184
|
+
# Normalise values as per F.6.2
|
181
185
|
rx = rx.abs
|
182
186
|
ry = ry.abs
|
183
187
|
phi = (phi % 360) * 2 * Math::PI / 360.0
|
184
188
|
|
189
|
+
# F.6.2: If the endpoints (x1, y1) and (x2, y2) are identical, then this is equivalent to omitting the elliptical arc segment entirely.
|
190
|
+
return if within_float_delta?(x1, x2) && within_float_delta?(y1, y2)
|
191
|
+
|
192
|
+
# F.6.2: If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a "lineto") joining the endpoints.
|
193
|
+
if within_float_delta?(rx, 0) || within_float_delta?(ry, 0)
|
194
|
+
@last_point = [x2, y2]
|
195
|
+
@calls << ["line_to", @last_point]
|
196
|
+
return
|
197
|
+
end
|
198
|
+
|
185
199
|
# We need to get the center co-ordinates, as well as the angles from the X axis to the start and end
|
186
200
|
# points. To do this, we use the algorithm documented in the SVG specification section F.6.5.
|
187
201
|
|
@@ -202,7 +216,7 @@ module Prawn
|
|
202
216
|
r2x = rx * rx
|
203
217
|
r2y = ry * ry
|
204
218
|
square = (r2x * r2y - r2x * yp1 * yp1 - r2y * xp1 * xp1) / (r2x * yp1 * yp1 + r2y * xp1 * xp1)
|
205
|
-
square = 0 if square < 0 && square > -
|
219
|
+
square = 0 if square < 0 && square > -FLOAT_ERROR_DELTA # catch rounding errors
|
206
220
|
base = Math.sqrt(square)
|
207
221
|
base *= -1 if fa == fs
|
208
222
|
cpx = base * rx * yp1 / ry
|
@@ -252,6 +266,10 @@ module Prawn
|
|
252
266
|
@previous_quadratic_control_point = nil unless %w(Q T).include?(upcase_command)
|
253
267
|
end
|
254
268
|
|
269
|
+
def within_float_delta?(a, b)
|
270
|
+
(a - b).abs < FLOAT_ERROR_DELTA
|
271
|
+
end
|
272
|
+
|
255
273
|
def match_all(string, regexp) # regexp must start with ^
|
256
274
|
result = []
|
257
275
|
while string != ""
|
data/lib/prawn/svg/parser.rb
CHANGED
@@ -13,6 +13,7 @@ require 'rexml/document'
|
|
13
13
|
#
|
14
14
|
class Prawn::Svg::Parser
|
15
15
|
CONTAINER_TAGS = %w(g svg symbol defs clipPath)
|
16
|
+
COMMA_WSP_REGEXP = /(?:\s+,?\s*|,\s*)/
|
16
17
|
|
17
18
|
#
|
18
19
|
# Construct a Parser object.
|
@@ -42,6 +43,19 @@ class Prawn::Svg::Parser
|
|
42
43
|
@document.warnings.clear
|
43
44
|
|
44
45
|
calls = [['fill_color', '000000', []]]
|
46
|
+
|
47
|
+
calls << [
|
48
|
+
'transformation_matrix',
|
49
|
+
[@document.sizing.x_scale, 0, 0, @document.sizing.y_scale, 0, 0],
|
50
|
+
[]
|
51
|
+
]
|
52
|
+
|
53
|
+
calls << [
|
54
|
+
'transformation_matrix',
|
55
|
+
[1, 0, 0, 1, @document.sizing.x_offset, @document.sizing.y_offset],
|
56
|
+
[]
|
57
|
+
]
|
58
|
+
|
45
59
|
root_element = Prawn::Svg::Element.new(@document, @document.root, calls, :ids => {}, :fill => true)
|
46
60
|
|
47
61
|
parse_element(root_element)
|
@@ -92,44 +106,51 @@ class Prawn::Svg::Parser
|
|
92
106
|
element.add_call 'line', x(attrs['x1'] || '0'), y(attrs['y1'] || '0'), x(attrs['x2'] || '0'), y(attrs['y2'] || '0')
|
93
107
|
|
94
108
|
when 'polyline'
|
95
|
-
points = attrs['points']
|
96
|
-
return unless
|
97
|
-
x, y =
|
109
|
+
points = parse_points(attrs['points'])
|
110
|
+
return unless points.length > 0
|
111
|
+
x, y = points.shift
|
98
112
|
element.add_call 'move_to', x(x), y(y)
|
99
113
|
element.add_call_and_enter 'stroke'
|
100
|
-
points.each do |
|
101
|
-
x, y = point.split(",")
|
114
|
+
points.each do |x, y|
|
102
115
|
element.add_call "line_to", x(x), y(y)
|
103
116
|
end
|
104
117
|
|
105
118
|
when 'polygon'
|
106
|
-
points = attrs['points']
|
107
|
-
x, y = point.split(",")
|
119
|
+
points = parse_points(attrs['points']).collect do |x, y|
|
108
120
|
[x(x), y(y)]
|
109
121
|
end
|
110
122
|
element.add_call "polygon", *points
|
111
123
|
|
112
124
|
when 'circle'
|
125
|
+
xy, r = [x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['r'])
|
126
|
+
|
127
|
+
return if zero_argument?(r)
|
128
|
+
|
113
129
|
if USE_NEW_CIRCLE_CALL
|
114
|
-
element.add_call "circle",
|
115
|
-
[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['r'])
|
130
|
+
element.add_call "circle", xy, r
|
116
131
|
else
|
117
|
-
element.add_call "circle_at",
|
118
|
-
[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], :radius => distance(attrs['r'])
|
132
|
+
element.add_call "circle_at", xy, :radius => r
|
119
133
|
end
|
120
134
|
|
121
135
|
when 'ellipse'
|
122
|
-
|
123
|
-
|
136
|
+
xy, rx, ry = [x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx'], :x), distance(attrs['ry'], :y)
|
137
|
+
|
138
|
+
return if zero_argument?(rx, ry)
|
139
|
+
|
140
|
+
element.add_call USE_NEW_ELLIPSE_CALL ? "ellipse" : "ellipse_at", xy, rx, ry
|
124
141
|
|
125
142
|
when 'rect'
|
126
|
-
|
127
|
-
|
143
|
+
xy = [x(attrs['x'] || '0'), y(attrs['y'] || '0')]
|
144
|
+
width, height = distance(attrs['width'], :x), distance(attrs['height'], :y)
|
145
|
+
radius = distance(attrs['rx'] || attrs['ry'])
|
146
|
+
|
147
|
+
return if zero_argument?(width, height)
|
148
|
+
|
128
149
|
if radius
|
129
150
|
# n.b. does not support both rx and ry being specified with different values
|
130
|
-
element.add_call "rounded_rectangle",
|
151
|
+
element.add_call "rounded_rectangle", xy, width, height, radius
|
131
152
|
else
|
132
|
-
element.add_call "rectangle",
|
153
|
+
element.add_call "rectangle", xy, width, height
|
133
154
|
end
|
134
155
|
|
135
156
|
when 'path'
|
@@ -191,7 +212,7 @@ class Prawn::Svg::Parser
|
|
191
212
|
x = element.attributes['x']
|
192
213
|
y = element.attributes['y']
|
193
214
|
if x || y
|
194
|
-
element.add_call_and_enter "translate", distance(x || 0), -distance(y || 0)
|
215
|
+
element.add_call_and_enter "translate", distance(x || 0, :x), -distance(y || 0, :y)
|
195
216
|
end
|
196
217
|
element.add_calls_from_element definition_element
|
197
218
|
else
|
@@ -227,7 +248,21 @@ class Prawn::Svg::Parser
|
|
227
248
|
missing_attrs.empty?
|
228
249
|
end
|
229
250
|
|
251
|
+
def zero_argument?(*args)
|
252
|
+
args.any? {|arg| arg.nil? || arg <= 0}
|
253
|
+
end
|
254
|
+
|
230
255
|
%w(x y distance).each do |method|
|
231
256
|
define_method(method) {|*a| @document.send(method, *a)}
|
232
257
|
end
|
258
|
+
|
259
|
+
def parse_points(points_string)
|
260
|
+
points_string.
|
261
|
+
to_s.
|
262
|
+
strip.
|
263
|
+
gsub(/(\d)-(\d)/, '\1 -\2').
|
264
|
+
split(COMMA_WSP_REGEXP).
|
265
|
+
each_slice(2).
|
266
|
+
to_a
|
267
|
+
end
|
233
268
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
class Prawn::Svg::UrlLoader
|
5
|
+
attr_accessor :enable_cache, :enable_web
|
6
|
+
attr_reader :url_cache
|
7
|
+
|
8
|
+
DATAURL_REGEXP = /(data:image\/(png|jpg);base64(;[a-z0-9]+)*,)/
|
9
|
+
URL_REGEXP = /^https?:\/\/|#{DATAURL_REGEXP}/
|
10
|
+
|
11
|
+
def initialize(opts = {})
|
12
|
+
@url_cache = {}
|
13
|
+
@enable_cache = opts.fetch(:enable_cache, false)
|
14
|
+
@enable_web = opts.fetch(:enable_web, true)
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid?(url)
|
18
|
+
!!url.match(URL_REGEXP)
|
19
|
+
end
|
20
|
+
|
21
|
+
def load(url)
|
22
|
+
@url_cache[url] || begin
|
23
|
+
if m = url.match(DATAURL_REGEXP)
|
24
|
+
data = Base64.decode64(url[m[0].length .. -1])
|
25
|
+
elsif enable_web
|
26
|
+
data = open(url).read
|
27
|
+
else
|
28
|
+
raise "No handler available to retrieve URL #{url}"
|
29
|
+
end
|
30
|
+
@url_cache[url] = data if enable_cache
|
31
|
+
data
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/prawn/svg/version.rb
CHANGED
data/lib/prawn-svg.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
require 'prawn'
|
2
2
|
require 'prawn/svg/version'
|
3
3
|
|
4
|
+
require 'prawn/svg/calculators/aspect_ratio'
|
5
|
+
require 'prawn/svg/calculators/document_sizing'
|
6
|
+
require 'prawn/svg/calculators/pixels'
|
7
|
+
require 'prawn/svg/url_loader'
|
4
8
|
require 'prawn/svg/color'
|
5
9
|
require 'prawn/svg/extension'
|
6
10
|
require 'prawn/svg/interface'
|
data/prawn-svg.gemspec
CHANGED
@@ -5,7 +5,7 @@ spec = Gem::Specification.new do |gem|
|
|
5
5
|
gem.name = 'prawn-svg'
|
6
6
|
gem.version = Prawn::Svg::VERSION
|
7
7
|
gem.summary = "SVG renderer for Prawn PDF library"
|
8
|
-
gem.description = "SVG
|
8
|
+
gem.description = "This gem allows you to render SVG directly into a PDF using the 'prawn' gem. Since PDF is vector-based, you'll get nice scaled graphics if you use SVG instead of an image."
|
9
9
|
gem.has_rdoc = false
|
10
10
|
gem.author = "Roger Nesbitt"
|
11
11
|
gem.email = "roger@seriousorange.com"
|
@@ -18,7 +18,10 @@ spec = Gem::Specification.new do |gem|
|
|
18
18
|
gem.name = "prawn-svg"
|
19
19
|
gem.require_paths = ["lib"]
|
20
20
|
|
21
|
-
gem.
|
22
|
-
|
23
|
-
gem.
|
21
|
+
gem.required_ruby_version = '>= 1.9.3'
|
22
|
+
|
23
|
+
gem.add_runtime_dependency "prawn", ">= 0.8.4", "< 3"
|
24
|
+
gem.add_runtime_dependency "css_parser", "~> 1.3"
|
25
|
+
gem.add_development_dependency "rspec", "~> 3.0"
|
26
|
+
gem.add_development_dependency "rake", "~> 10.1"
|
24
27
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Prawn::Svg::Interface do
|
4
|
+
root = "#{File.dirname(__FILE__)}/.."
|
5
|
+
|
6
|
+
context "with option :position" do
|
7
|
+
let(:svg) { IO.read("#{root}/spec/sample_svg/cubic01a.svg") }
|
8
|
+
|
9
|
+
it "aligns the image as requested" do
|
10
|
+
Prawn::Document.generate("#{root}/spec/sample_output/_with_position.pdf") do |prawn|
|
11
|
+
width = prawn.bounds.width / 3
|
12
|
+
|
13
|
+
prawn.svg svg, :width => width, :position => :left
|
14
|
+
prawn.svg svg, :width => width, :position => :center
|
15
|
+
prawn.svg svg, :width => width, :position => :right
|
16
|
+
prawn.svg svg, :width => width, :position => 50
|
17
|
+
prawn.svg svg, :width => width
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "with option :vposition" do
|
23
|
+
let(:svg) { IO.read("#{root}/spec/sample_svg/cubic01a.svg") }
|
24
|
+
|
25
|
+
it "aligns the image as requested" do
|
26
|
+
Prawn::Document.generate("#{root}/spec/sample_output/_with_vposition.pdf") do |prawn|
|
27
|
+
width = prawn.bounds.width / 3
|
28
|
+
|
29
|
+
prawn.svg svg, :width => width, :position => :left, :vposition => :bottom
|
30
|
+
prawn.svg svg, :width => width, :position => :center, :vposition => :center
|
31
|
+
prawn.svg svg, :width => width, :position => :right, :vposition => :top
|
32
|
+
prawn.svg svg, :width => width, :position => 50, :vposition => 50
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "sample file rendering" do
|
38
|
+
files = Dir["#{root}/spec/sample_svg/*.svg"]
|
39
|
+
|
40
|
+
it "has at least 10 SVG sample files to test" do
|
41
|
+
files.length.should >= 10
|
42
|
+
end
|
43
|
+
|
44
|
+
files.each do |file|
|
45
|
+
it "renders the #{File.basename file} sample file without warnings or crashing" do
|
46
|
+
warnings = nil
|
47
|
+
Prawn::Document.generate("#{root}/spec/sample_output/#{File.basename file}.pdf") do |prawn|
|
48
|
+
r = prawn.svg IO.read(file), :at => [0, prawn.bounds.top], :width => prawn.bounds.width do |doc|
|
49
|
+
doc.url_loader.enable_web = false
|
50
|
+
doc.url_loader.url_cache["https://raw.githubusercontent.com/mogest/prawn-svg/master/spec/sample_images/mushroom-wide.jpg"] = IO.read("#{root}/spec/sample_images/mushroom-wide.jpg")
|
51
|
+
doc.url_loader.url_cache["https://raw.githubusercontent.com/mogest/prawn-svg/master/spec/sample_images/mushroom-long.jpg"] = IO.read("#{root}/spec/sample_images/mushroom-long.jpg")
|
52
|
+
end
|
53
|
+
|
54
|
+
warnings = r[:warnings].reject {|w| w =~ /Verdana/ && w =~ /is not a known font/ }
|
55
|
+
end
|
56
|
+
warnings.should == []
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "multiple file rendering" do
|
62
|
+
it "renders multiple files on to the same PDF" do
|
63
|
+
Prawn::Document.generate("#{root}/spec/sample_output/_multiple.pdf") do |prawn|
|
64
|
+
width = prawn.bounds.width
|
65
|
+
|
66
|
+
y = prawn.bounds.top - 12
|
67
|
+
prawn.draw_text "This is multiple SVGs being output to the same PDF", :at => [0, y]
|
68
|
+
|
69
|
+
y -= 12
|
70
|
+
prawn.svg IO.read("#{root}/spec/sample_svg/arcs01.svg"), :at => [0, y], :width => width / 2
|
71
|
+
prawn.svg IO.read("#{root}/spec/sample_svg/circle01.svg"), :at => [width / 2, y], :width => width / 2
|
72
|
+
|
73
|
+
y -= 120
|
74
|
+
prawn.draw_text "Here are some more PDFs below", :at => [0, y]
|
75
|
+
|
76
|
+
y -= 12
|
77
|
+
prawn.svg IO.read("#{root}/spec/sample_svg/quad01.svg"), :at => [0, y], :width => width / 3
|
78
|
+
prawn.svg IO.read("#{root}/spec/sample_svg/rect01.svg"), :at => [width / 3, y], :width => width / 3
|
79
|
+
prawn.svg IO.read("#{root}/spec/sample_svg/rect02.svg"), :at => [width / 3 * 2, y], :width => width / 3
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|