prawn-svg 0.15.0.0 → 0.19.0
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.
- 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
|