prawn-svg 0.15.0.0 → 0.23.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +6 -0
- data/README.md +51 -20
- data/lib/prawn/svg/attributes/clip_path.rb +17 -0
- data/lib/prawn/svg/attributes/color.rb +5 -0
- data/lib/prawn/svg/attributes/display.rb +5 -0
- data/lib/prawn/svg/attributes/font.rb +38 -0
- data/lib/prawn/svg/attributes/opacity.rb +15 -0
- data/lib/prawn/svg/attributes/stroke.rb +35 -0
- data/lib/prawn/svg/attributes/transform.rb +50 -0
- data/lib/prawn/svg/attributes.rb +6 -0
- data/lib/prawn/svg/calculators/aspect_ratio.rb +58 -0
- data/lib/prawn/svg/calculators/document_sizing.rb +99 -0
- data/lib/prawn/svg/calculators/pixels.rb +21 -0
- data/lib/prawn/svg/color.rb +197 -12
- data/lib/prawn/svg/css.rb +40 -0
- data/lib/prawn/svg/document.rb +37 -48
- data/lib/prawn/svg/elements/base.rb +238 -0
- data/lib/prawn/svg/elements/circle.rb +25 -0
- data/lib/prawn/svg/elements/container.rb +15 -0
- data/lib/prawn/svg/elements/ellipse.rb +23 -0
- data/lib/prawn/svg/elements/gradient.rb +120 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +81 -0
- data/lib/prawn/svg/elements/line.rb +16 -0
- data/lib/prawn/svg/elements/path.rb +405 -0
- data/lib/prawn/svg/elements/polygon.rb +17 -0
- data/lib/prawn/svg/elements/polyline.rb +22 -0
- data/lib/prawn/svg/elements/rect.rb +33 -0
- data/lib/prawn/svg/elements/root.rb +13 -0
- data/lib/prawn/svg/elements/style.rb +10 -0
- data/lib/prawn/svg/elements/text.rb +87 -0
- data/lib/prawn/svg/elements/use.rb +29 -0
- data/lib/prawn/svg/elements.rb +33 -0
- data/lib/prawn/svg/extension.rb +4 -4
- data/lib/prawn/svg/font.rb +10 -92
- data/lib/prawn/svg/font_registry.rb +73 -0
- data/lib/prawn/svg/interface.rb +91 -31
- data/lib/prawn/svg/loaders/data.rb +18 -0
- data/lib/prawn/svg/loaders/file.rb +66 -0
- data/lib/prawn/svg/loaders/web.rb +28 -0
- data/lib/prawn/svg/state.rb +39 -0
- data/lib/prawn/svg/ttf.rb +61 -0
- data/lib/prawn/svg/url_loader.rb +46 -0
- data/lib/prawn/svg/version.rb +2 -2
- data/lib/prawn-svg.rb +20 -6
- data/prawn-svg.gemspec +8 -5
- data/spec/integration_spec.rb +141 -0
- data/spec/prawn/svg/attributes/font_spec.rb +52 -0
- data/spec/prawn/svg/attributes/transform_spec.rb +56 -0
- data/spec/prawn/svg/calculators/aspect_ratio_spec.rb +95 -0
- data/spec/prawn/svg/calculators/document_sizing_spec.rb +128 -0
- data/spec/prawn/svg/color_spec.rb +43 -8
- data/spec/prawn/svg/css_spec.rb +24 -0
- data/spec/prawn/svg/document_spec.rb +48 -19
- data/spec/prawn/svg/elements/base_spec.rb +147 -0
- data/spec/prawn/svg/elements/gradient_spec.rb +61 -0
- data/spec/prawn/svg/elements/path_spec.rb +123 -0
- data/spec/prawn/svg/elements/style_spec.rb +23 -0
- data/spec/prawn/svg/elements/text_spec.rb +61 -0
- data/spec/prawn/svg/font_registry_spec.rb +54 -0
- data/spec/prawn/svg/font_spec.rb +1 -28
- data/spec/prawn/svg/interface_spec.rb +94 -0
- data/spec/prawn/svg/loaders/data_spec.rb +55 -0
- data/spec/prawn/svg/loaders/file_spec.rb +84 -0
- data/spec/prawn/svg/loaders/web_spec.rb +37 -0
- data/spec/prawn/svg/ttf_spec.rb +32 -0
- data/spec/prawn/svg/url_loader_spec.rb +112 -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/gradients.svg +40 -0
- data/spec/sample_svg/hidden_paths.svg +6 -0
- data/spec/sample_svg/image01.svg +31 -31
- data/spec/sample_svg/image03.svg +30 -0
- data/spec/sample_svg/negminy.svg +25 -0
- data/spec/sample_svg/no_width_or_height.svg +4 -0
- data/spec/sample_svg/path.svg +5 -0
- data/spec/sample_svg/pie_piece.svg +7 -0
- data/spec/sample_svg/preserve-space.svg +19 -0
- data/spec/sample_svg/rect02.svg +8 -11
- data/spec/sample_svg/tspan03-cc.svg +21 -0
- data/spec/sample_svg/viewbox.svg +4 -0
- data/spec/sample_svg/viewport.svg +23 -0
- data/spec/sample_ttf/OpenSans-SemiboldItalic.ttf +0 -0
- data/spec/spec_helper.rb +24 -2
- metadata +150 -36
- data/lib/prawn/svg/element.rb +0 -237
- data/lib/prawn/svg/parser/image.rb +0 -134
- data/lib/prawn/svg/parser/path.rb +0 -374
- data/lib/prawn/svg/parser/text.rb +0 -66
- data/lib/prawn/svg/parser.rb +0 -233
- data/spec/lib/parser_spec.rb +0 -55
- data/spec/lib/path_spec.rb +0 -54
- data/spec/lib/svg_spec.rb +0 -47
- data/spec/prawn/svg/element_spec.rb +0 -36
- data/spec/prawn/svg/parser/text_spec.rb +0 -4
@@ -1,374 +0,0 @@
|
|
1
|
-
module Prawn
|
2
|
-
module Svg
|
3
|
-
class Parser::Path
|
4
|
-
# Raised if the SVG path cannot be parsed.
|
5
|
-
InvalidError = Class.new(StandardError)
|
6
|
-
|
7
|
-
INSIDE_SPACE_REGEXP = /[ \t\r\n,]*/
|
8
|
-
OUTSIDE_SPACE_REGEXP = /[ \t\r\n]*/
|
9
|
-
INSIDE_REGEXP = /#{INSIDE_SPACE_REGEXP}([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:(?<=[0-9])e[+-]?[0-9]+)?)/
|
10
|
-
VALUES_REGEXP = /^#{INSIDE_REGEXP}/
|
11
|
-
COMMAND_REGEXP = /^#{OUTSIDE_SPACE_REGEXP}([A-Za-z])((?:#{INSIDE_REGEXP})*)#{OUTSIDE_SPACE_REGEXP}/
|
12
|
-
|
13
|
-
|
14
|
-
#
|
15
|
-
# Parses an SVG path and returns a Prawn-compatible call tree.
|
16
|
-
#
|
17
|
-
def parse(data)
|
18
|
-
@subpath_initial_point = @last_point = nil
|
19
|
-
@previous_control_point = @previous_quadratic_control_point = nil
|
20
|
-
@calls = []
|
21
|
-
|
22
|
-
data = data.gsub(/#{OUTSIDE_SPACE_REGEXP}$/, '')
|
23
|
-
|
24
|
-
matched_commands = match_all(data, COMMAND_REGEXP)
|
25
|
-
raise InvalidError, "Invalid/unsupported syntax for SVG path data" if matched_commands.nil?
|
26
|
-
|
27
|
-
matched_commands.each do |matched_command|
|
28
|
-
command = matched_command[1]
|
29
|
-
matched_values = match_all(matched_command[2], VALUES_REGEXP)
|
30
|
-
raise "should be impossible to have invalid inside data, but we ended up here" if matched_values.nil?
|
31
|
-
values = matched_values.collect {|value| value[1].to_f}
|
32
|
-
run_path_command(command, values)
|
33
|
-
end
|
34
|
-
|
35
|
-
@calls
|
36
|
-
end
|
37
|
-
|
38
|
-
|
39
|
-
private
|
40
|
-
def run_path_command(command, values)
|
41
|
-
upcase_command = command.upcase
|
42
|
-
relative = command != upcase_command
|
43
|
-
|
44
|
-
case upcase_command
|
45
|
-
when 'M' # moveto
|
46
|
-
x = values.shift
|
47
|
-
y = values.shift
|
48
|
-
|
49
|
-
if relative && @last_point
|
50
|
-
x += @last_point.first
|
51
|
-
y += @last_point.last
|
52
|
-
end
|
53
|
-
|
54
|
-
@last_point = @subpath_initial_point = [x, y]
|
55
|
-
@calls << ["move_to", @last_point]
|
56
|
-
|
57
|
-
return run_path_command(relative ? 'l' : 'L', values) if values.any?
|
58
|
-
|
59
|
-
when 'Z' # closepath
|
60
|
-
if @subpath_initial_point
|
61
|
-
#@calls << ["line_to", @subpath_initial_point]
|
62
|
-
@calls << ["close_path"]
|
63
|
-
@last_point = @subpath_initial_point
|
64
|
-
end
|
65
|
-
|
66
|
-
when 'L' # lineto
|
67
|
-
while values.any?
|
68
|
-
x = values.shift
|
69
|
-
y = values.shift
|
70
|
-
if relative && @last_point
|
71
|
-
x += @last_point.first
|
72
|
-
y += @last_point.last
|
73
|
-
end
|
74
|
-
@last_point = [x, y]
|
75
|
-
@calls << ["line_to", @last_point]
|
76
|
-
end
|
77
|
-
|
78
|
-
when 'H' # horizontal lineto
|
79
|
-
while values.any?
|
80
|
-
x = values.shift
|
81
|
-
x += @last_point.first if relative && @last_point
|
82
|
-
@last_point = [x, @last_point.last]
|
83
|
-
@calls << ["line_to", @last_point]
|
84
|
-
end
|
85
|
-
|
86
|
-
when 'V' # vertical lineto
|
87
|
-
while values.any?
|
88
|
-
y = values.shift
|
89
|
-
y += @last_point.last if relative && @last_point
|
90
|
-
@last_point = [@last_point.first, y]
|
91
|
-
@calls << ["line_to", @last_point]
|
92
|
-
end
|
93
|
-
|
94
|
-
when 'C' # curveto
|
95
|
-
while values.any?
|
96
|
-
x1, y1, x2, y2, x, y = (1..6).collect {values.shift}
|
97
|
-
if relative && @last_point
|
98
|
-
x += @last_point.first
|
99
|
-
x1 += @last_point.first
|
100
|
-
x2 += @last_point.first
|
101
|
-
y += @last_point.last
|
102
|
-
y1 += @last_point.last
|
103
|
-
y2 += @last_point.last
|
104
|
-
end
|
105
|
-
|
106
|
-
@last_point = [x, y]
|
107
|
-
@previous_control_point = [x2, y2]
|
108
|
-
@calls << ["curve_to", [x, y, x1, y1, x2, y2]]
|
109
|
-
end
|
110
|
-
|
111
|
-
when 'S' # shorthand/smooth curveto
|
112
|
-
while values.any?
|
113
|
-
x2, y2, x, y = (1..4).collect {values.shift}
|
114
|
-
if relative && @last_point
|
115
|
-
x += @last_point.first
|
116
|
-
x2 += @last_point.first
|
117
|
-
y += @last_point.last
|
118
|
-
y2 += @last_point.last
|
119
|
-
end
|
120
|
-
|
121
|
-
if @previous_control_point
|
122
|
-
x1 = 2 * @last_point.first - @previous_control_point.first
|
123
|
-
y1 = 2 * @last_point.last - @previous_control_point.last
|
124
|
-
else
|
125
|
-
x1, y1 = @last_point
|
126
|
-
end
|
127
|
-
|
128
|
-
@last_point = [x, y]
|
129
|
-
@previous_control_point = [x2, y2]
|
130
|
-
@calls << ["curve_to", [x, y, x1, y1, x2, y2]]
|
131
|
-
end
|
132
|
-
|
133
|
-
when 'Q', 'T' # quadratic curveto
|
134
|
-
while values.any?
|
135
|
-
if shorthand = upcase_command == 'T'
|
136
|
-
x, y = (1..2).collect {values.shift}
|
137
|
-
else
|
138
|
-
x1, y1, x, y = (1..4).collect {values.shift}
|
139
|
-
end
|
140
|
-
|
141
|
-
if relative && @last_point
|
142
|
-
x += @last_point.first
|
143
|
-
x1 += @last_point.first if x1
|
144
|
-
y += @last_point.last
|
145
|
-
y1 += @last_point.last if y1
|
146
|
-
end
|
147
|
-
|
148
|
-
if shorthand
|
149
|
-
if @previous_quadratic_control_point
|
150
|
-
x1 = 2 * @last_point.first - @previous_quadratic_control_point.first
|
151
|
-
y1 = 2 * @last_point.last - @previous_quadratic_control_point.last
|
152
|
-
else
|
153
|
-
x1, y1 = @last_point
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
# convert from quadratic to cubic
|
158
|
-
cx1 = @last_point.first + (x1 - @last_point.first) * 2 / 3.0
|
159
|
-
cy1 = @last_point.last + (y1 - @last_point.last) * 2 / 3.0
|
160
|
-
cx2 = cx1 + (x - @last_point.first) / 3.0
|
161
|
-
cy2 = cy1 + (y - @last_point.last) / 3.0
|
162
|
-
|
163
|
-
@last_point = [x, y]
|
164
|
-
@previous_quadratic_control_point = [x1, y1]
|
165
|
-
|
166
|
-
@calls << ["curve_to", [x, y, cx1, cy1, cx2, cy2]]
|
167
|
-
end
|
168
|
-
|
169
|
-
when 'A'
|
170
|
-
return unless @last_point
|
171
|
-
|
172
|
-
while values.any?
|
173
|
-
rx, ry, phi, fa, fs, x2, y2 = (1..7).collect {values.shift}
|
174
|
-
x1, y1 = @last_point
|
175
|
-
|
176
|
-
if relative
|
177
|
-
x2 += x1
|
178
|
-
y2 += y1
|
179
|
-
end
|
180
|
-
|
181
|
-
rx = rx.abs
|
182
|
-
ry = ry.abs
|
183
|
-
phi = (phi % 360) * 2 * Math::PI / 360.0
|
184
|
-
|
185
|
-
# We need to get the center co-ordinates, as well as the angles from the X axis to the start and end
|
186
|
-
# points. To do this, we use the algorithm documented in the SVG specification section F.6.5.
|
187
|
-
|
188
|
-
# F.6.5.1
|
189
|
-
xp1 = Math.cos(phi) * ((x1-x2)/2.0) + Math.sin(phi) * ((y1-y2)/2.0)
|
190
|
-
yp1 = -Math.sin(phi) * ((x1-x2)/2.0) + Math.cos(phi) * ((y1-y2)/2.0)
|
191
|
-
|
192
|
-
# F.6.6.2
|
193
|
-
r2x = rx * rx
|
194
|
-
r2y = ry * ry
|
195
|
-
hat = xp1 * xp1 / r2x + yp1 * yp1 / r2y
|
196
|
-
if hat > 1
|
197
|
-
rx *= Math.sqrt(hat)
|
198
|
-
ry *= Math.sqrt(hat)
|
199
|
-
end
|
200
|
-
|
201
|
-
# F.6.5.2
|
202
|
-
r2x = rx * rx
|
203
|
-
r2y = ry * ry
|
204
|
-
square = (r2x * r2y - r2x * yp1 * yp1 - r2y * xp1 * xp1) / (r2x * yp1 * yp1 + r2y * xp1 * xp1)
|
205
|
-
square = 0 if square < 0 && square > -1e-10 # catch rounding errors
|
206
|
-
base = Math.sqrt(square)
|
207
|
-
base *= -1 if fa == fs
|
208
|
-
cpx = base * rx * yp1 / ry
|
209
|
-
cpy = base * -ry * xp1 / rx
|
210
|
-
|
211
|
-
# F.6.5.3
|
212
|
-
cx = Math.cos(phi) * cpx + -Math.sin(phi) * cpy + (x1 + x2) / 2
|
213
|
-
cy = Math.sin(phi) * cpx + Math.cos(phi) * cpy + (y1 + y2) / 2
|
214
|
-
|
215
|
-
# F.6.5.5
|
216
|
-
vx = (xp1 - cpx) / rx
|
217
|
-
vy = (yp1 - cpy) / ry
|
218
|
-
theta_1 = Math.acos(vx / Math.sqrt(vx * vx + vy * vy))
|
219
|
-
theta_1 *= -1 if vy < 0
|
220
|
-
|
221
|
-
# F.6.5.6
|
222
|
-
ux = vx
|
223
|
-
uy = vy
|
224
|
-
vx = (-xp1 - cpx) / rx
|
225
|
-
vy = (-yp1 - cpy) / ry
|
226
|
-
|
227
|
-
numerator = ux * vx + uy * vy
|
228
|
-
denominator = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy)
|
229
|
-
division = numerator / denominator
|
230
|
-
division = -1 if division < -1 # for rounding errors
|
231
|
-
|
232
|
-
d_theta = Math.acos(division) % (2 * Math::PI)
|
233
|
-
d_theta *= -1 if ux * vy - uy * vx < 0
|
234
|
-
|
235
|
-
# Adjust range
|
236
|
-
if fs == 0
|
237
|
-
d_theta -= 2 * Math::PI if d_theta > 0
|
238
|
-
else
|
239
|
-
d_theta += 2 * Math::PI if d_theta < 0
|
240
|
-
end
|
241
|
-
|
242
|
-
theta_2 = theta_1 + d_theta
|
243
|
-
|
244
|
-
calculate_bezier_curve_points_for_arc(cx, cy, rx, ry, theta_1, theta_2, phi).each do |points|
|
245
|
-
@calls << ["curve_to", points[:p2] + points[:q1] + points[:q2]]
|
246
|
-
@last_point = points[:p2]
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
@previous_control_point = nil unless %w(C S).include?(upcase_command)
|
252
|
-
@previous_quadratic_control_point = nil unless %w(Q T).include?(upcase_command)
|
253
|
-
end
|
254
|
-
|
255
|
-
def match_all(string, regexp) # regexp must start with ^
|
256
|
-
result = []
|
257
|
-
while string != ""
|
258
|
-
matches = string.match(regexp)
|
259
|
-
result << matches
|
260
|
-
return if matches.nil?
|
261
|
-
string = matches.post_match
|
262
|
-
end
|
263
|
-
result
|
264
|
-
end
|
265
|
-
|
266
|
-
def calculate_eta_from_lambda(a, b, lambda_1, lambda_2)
|
267
|
-
# 2.2.1
|
268
|
-
eta1 = Math.atan2(Math.sin(lambda_1) / b, Math.cos(lambda_1) / a)
|
269
|
-
eta2 = Math.atan2(Math.sin(lambda_2) / b, Math.cos(lambda_2) / a)
|
270
|
-
|
271
|
-
# ensure eta1 <= eta2 <= eta1 + 2*PI
|
272
|
-
eta2 -= 2 * Math::PI * ((eta2 - eta1) / (2 * Math::PI)).floor
|
273
|
-
eta2 += 2 * Math::PI if lambda_2 - lambda_1 > Math::PI && eta2 - eta1 < Math::PI
|
274
|
-
|
275
|
-
[eta1, eta2]
|
276
|
-
end
|
277
|
-
|
278
|
-
# Convert the elliptical arc to a cubic bézier curve using this algorithm:
|
279
|
-
# http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
|
280
|
-
def calculate_bezier_curve_points_for_arc(cx, cy, a, b, lambda_1, lambda_2, theta)
|
281
|
-
e = lambda do |eta|
|
282
|
-
[
|
283
|
-
cx + a * Math.cos(theta) * Math.cos(eta) - b * Math.sin(theta) * Math.sin(eta),
|
284
|
-
cy + a * Math.sin(theta) * Math.cos(eta) + b * Math.cos(theta) * Math.sin(eta)
|
285
|
-
]
|
286
|
-
end
|
287
|
-
|
288
|
-
ep = lambda do |eta|
|
289
|
-
[
|
290
|
-
-a * Math.cos(theta) * Math.sin(eta) - b * Math.sin(theta) * Math.cos(eta),
|
291
|
-
-a * Math.sin(theta) * Math.sin(eta) + b * Math.cos(theta) * Math.cos(eta)
|
292
|
-
]
|
293
|
-
end
|
294
|
-
|
295
|
-
iterations = 1
|
296
|
-
d_lambda = lambda_2 - lambda_1
|
297
|
-
|
298
|
-
while iterations < 1024
|
299
|
-
if d_lambda.abs <= Math::PI / 2.0
|
300
|
-
# TODO : run error algorithm, see whether it meets threshold or not
|
301
|
-
# puts "error = #{calculate_curve_approximation_error(a, b, eta1, eta1 + d_eta)}"
|
302
|
-
break
|
303
|
-
end
|
304
|
-
iterations *= 2
|
305
|
-
d_lambda = (lambda_2 - lambda_1) / iterations
|
306
|
-
end
|
307
|
-
|
308
|
-
(0...iterations).collect do |iteration|
|
309
|
-
eta_a, eta_b = calculate_eta_from_lambda(a, b, lambda_1+iteration*d_lambda, lambda_1+(iteration+1)*d_lambda)
|
310
|
-
d_eta = eta_b - eta_a
|
311
|
-
|
312
|
-
alpha = Math.sin(d_eta) * ((Math.sqrt(4 + 3 * Math.tan(d_eta / 2) ** 2) - 1) / 3)
|
313
|
-
|
314
|
-
x1, y1 = e[eta_a]
|
315
|
-
x2, y2 = e[eta_b]
|
316
|
-
|
317
|
-
ep_eta1_x, ep_eta1_y = ep[eta_a]
|
318
|
-
q1_x = x1 + alpha * ep_eta1_x
|
319
|
-
q1_y = y1 + alpha * ep_eta1_y
|
320
|
-
|
321
|
-
ep_eta2_x, ep_eta2_y = ep[eta_b]
|
322
|
-
q2_x = x2 - alpha * ep_eta2_x
|
323
|
-
q2_y = y2 - alpha * ep_eta2_y
|
324
|
-
|
325
|
-
{:p2 => [x2, y2], :q1 => [q1_x, q1_y], :q2 => [q2_x, q2_y]}
|
326
|
-
end
|
327
|
-
end
|
328
|
-
|
329
|
-
ERROR_COEFFICIENTS_A = [
|
330
|
-
[
|
331
|
-
[3.85268, -21.229, -0.330434, 0.0127842],
|
332
|
-
[-1.61486, 0.706564, 0.225945, 0.263682],
|
333
|
-
[-0.910164, 0.388383, 0.00551445, 0.00671814],
|
334
|
-
[-0.630184, 0.192402, 0.0098871, 0.0102527]
|
335
|
-
],
|
336
|
-
[
|
337
|
-
[-0.162211, 9.94329, 0.13723, 0.0124084],
|
338
|
-
[-0.253135, 0.00187735, 0.0230286, 0.01264],
|
339
|
-
[-0.0695069, -0.0437594, 0.0120636, 0.0163087],
|
340
|
-
[-0.0328856, -0.00926032, -0.00173573, 0.00527385]
|
341
|
-
]
|
342
|
-
]
|
343
|
-
|
344
|
-
ERROR_COEFFICIENTS_B = [
|
345
|
-
[
|
346
|
-
[0.0899116, -19.2349, -4.11711, 0.183362],
|
347
|
-
[0.138148, -1.45804, 1.32044, 1.38474],
|
348
|
-
[0.230903, -0.450262, 0.219963, 0.414038],
|
349
|
-
[0.0590565, -0.101062, 0.0430592, 0.0204699]
|
350
|
-
],
|
351
|
-
[
|
352
|
-
[0.0164649, 9.89394, 0.0919496, 0.00760802],
|
353
|
-
[0.0191603, -0.0322058, 0.0134667, -0.0825018],
|
354
|
-
[0.0156192, -0.017535, 0.00326508, -0.228157],
|
355
|
-
[-0.0236752, 0.0405821, -0.0173086, 0.176187]
|
356
|
-
]
|
357
|
-
]
|
358
|
-
|
359
|
-
def calculate_curve_approximation_error(a, b, eta1, eta2)
|
360
|
-
b_over_a = b / a
|
361
|
-
coefficents = b_over_a < 0.25 ? ERROR_COEFFICIENTS_A : ERROR_COEFFICIENTS_B
|
362
|
-
|
363
|
-
c = lambda do |i|
|
364
|
-
(0..3).inject(0) do |accumulator, j|
|
365
|
-
coef = coefficents[i][j]
|
366
|
-
accumulator + ((coef[0] * b_over_a**2 + coef[1] * b_over_a + coef[2]) / (b_over_a * coef[3])) * Math.cos(j * (eta1 + eta2))
|
367
|
-
end
|
368
|
-
end
|
369
|
-
|
370
|
-
((0.001 * b_over_a**2 + 4.98 * b_over_a + 0.207) / (b_over_a * 0.0067)) * a * Math.exp(c[0] + c[1] * (eta2 - eta1))
|
371
|
-
end
|
372
|
-
end
|
373
|
-
end
|
374
|
-
end
|
@@ -1,66 +0,0 @@
|
|
1
|
-
class Prawn::Svg::Parser::Text
|
2
|
-
def parse(element)
|
3
|
-
element.add_call_and_enter "text_group"
|
4
|
-
internal_parse(element, [element.document.x(0)], [element.document.y(0)], false)
|
5
|
-
end
|
6
|
-
|
7
|
-
protected
|
8
|
-
def internal_parse(element, x_positions, y_positions, relative)
|
9
|
-
attrs = element.attributes
|
10
|
-
|
11
|
-
if attrs['x'] || attrs['y']
|
12
|
-
relative = false
|
13
|
-
x_positions = attrs['x'].split(/[\s,]+/).collect {|n| element.document.x(n)} if attrs['x']
|
14
|
-
y_positions = attrs['y'].split(/[\s,]+/).collect {|n| element.document.y(n)} if attrs['y']
|
15
|
-
end
|
16
|
-
|
17
|
-
if attrs['dx'] || attrs['dy']
|
18
|
-
element.add_call_and_enter "translate", element.document.distance(attrs['dx'] || 0), -element.document.distance(attrs['dy'] || 0)
|
19
|
-
end
|
20
|
-
|
21
|
-
opts = {}
|
22
|
-
if size = element.state[:font_size]
|
23
|
-
opts[:size] = size
|
24
|
-
end
|
25
|
-
opts[:style] = element.state[:font_subfamily]
|
26
|
-
|
27
|
-
# This is not a prawn option but we can't work out how to render it here -
|
28
|
-
# it's handled by Svg#rewrite_call_arguments
|
29
|
-
if anchor = attrs['text-anchor']
|
30
|
-
opts[:text_anchor] = anchor
|
31
|
-
end
|
32
|
-
|
33
|
-
element.element.children.each do |child|
|
34
|
-
if child.node_type == :text
|
35
|
-
text = child.value.strip.gsub(/\s+/, " ")
|
36
|
-
|
37
|
-
while text != ""
|
38
|
-
opts[:at] = [x_positions.first, y_positions.first]
|
39
|
-
|
40
|
-
if x_positions.length > 1 || y_positions.length > 1
|
41
|
-
element.add_call 'draw_text', text[0..0], opts.dup
|
42
|
-
text = text[1..-1]
|
43
|
-
|
44
|
-
x_positions.shift if x_positions.length > 1
|
45
|
-
y_positions.shift if y_positions.length > 1
|
46
|
-
else
|
47
|
-
element.add_call relative ? 'relative_draw_text' : 'draw_text', text, opts.dup
|
48
|
-
relative = true
|
49
|
-
break
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
elsif child.name == "tspan"
|
54
|
-
element.add_call 'save'
|
55
|
-
child.attributes['text-anchor'] ||= opts[:text_anchor] if opts[:text_anchor]
|
56
|
-
child_element = Prawn::Svg::Element.new(element.document, child, element.calls, element.state.dup)
|
57
|
-
internal_parse(child_element, x_positions, y_positions, relative)
|
58
|
-
child_element.append_calls_to_parent
|
59
|
-
element.add_call 'restore'
|
60
|
-
|
61
|
-
else
|
62
|
-
element.warnings << "Unknown tag '#{child.name}' inside text tag; ignoring"
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
data/lib/prawn/svg/parser.rb
DELETED
@@ -1,233 +0,0 @@
|
|
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
|
-
CONTAINER_TAGS = %w(g svg symbol defs clipPath)
|
16
|
-
|
17
|
-
#
|
18
|
-
# Construct a Parser object.
|
19
|
-
#
|
20
|
-
# The +data+ argument is SVG data.
|
21
|
-
#
|
22
|
-
# +bounds+ is a tuple [width, height] that specifies the bounds of the drawing space in points.
|
23
|
-
#
|
24
|
-
# +options+ can optionally contain
|
25
|
-
# the key :width or :height. If both are specified, only :width will be used.
|
26
|
-
#
|
27
|
-
def initialize(document)
|
28
|
-
@document = document
|
29
|
-
end
|
30
|
-
|
31
|
-
#
|
32
|
-
# Parse the SVG data and return a call tree. The returned +Array+ is in the format:
|
33
|
-
#
|
34
|
-
# [
|
35
|
-
# ['prawn_method_name', ['argument1', 'argument2'], []],
|
36
|
-
# ['method_that_takes_a_block', ['argument1', 'argument2'], [
|
37
|
-
# ['method_called_inside_block', ['argument'], []]
|
38
|
-
# ]
|
39
|
-
# ]
|
40
|
-
#
|
41
|
-
def parse
|
42
|
-
@document.warnings.clear
|
43
|
-
|
44
|
-
calls = [['fill_color', '000000', []]]
|
45
|
-
root_element = Prawn::Svg::Element.new(@document, @document.root, calls, :ids => {}, :fill => true)
|
46
|
-
|
47
|
-
parse_element(root_element)
|
48
|
-
calls
|
49
|
-
end
|
50
|
-
|
51
|
-
|
52
|
-
private
|
53
|
-
REQUIRED_ATTRIBUTES = {
|
54
|
-
"polyline" => %w(points),
|
55
|
-
"polygon" => %w(points),
|
56
|
-
"circle" => %w(r),
|
57
|
-
"ellipse" => %w(rx ry),
|
58
|
-
"rect" => %w(width height),
|
59
|
-
"path" => %w(d),
|
60
|
-
"image" => %w(width height)
|
61
|
-
}
|
62
|
-
|
63
|
-
USE_NEW_CIRCLE_CALL = Prawn::Document.new.respond_to?(:circle)
|
64
|
-
USE_NEW_ELLIPSE_CALL = Prawn::Document.new.respond_to?(:ellipse)
|
65
|
-
|
66
|
-
def parse_element(element)
|
67
|
-
attrs = element.attributes
|
68
|
-
|
69
|
-
if required_attributes = REQUIRED_ATTRIBUTES[element.name]
|
70
|
-
return unless check_attrs_present(element, required_attributes)
|
71
|
-
end
|
72
|
-
|
73
|
-
case element.name
|
74
|
-
when *CONTAINER_TAGS
|
75
|
-
do_not_append_calls = %w(symbol defs clipPath).include?(element.name)
|
76
|
-
element.state[:disable_drawing] = true if element.name == "clipPath"
|
77
|
-
|
78
|
-
element.each_child_element do |child|
|
79
|
-
element.add_call "save"
|
80
|
-
parse_element(child)
|
81
|
-
element.add_call "restore"
|
82
|
-
end
|
83
|
-
|
84
|
-
when 'style'
|
85
|
-
load_css_styles(element)
|
86
|
-
|
87
|
-
when 'text'
|
88
|
-
@svg_text ||= Text.new
|
89
|
-
@svg_text.parse(element)
|
90
|
-
|
91
|
-
when 'line'
|
92
|
-
element.add_call 'line', x(attrs['x1'] || '0'), y(attrs['y1'] || '0'), x(attrs['x2'] || '0'), y(attrs['y2'] || '0')
|
93
|
-
|
94
|
-
when 'polyline'
|
95
|
-
points = attrs['points'].split(/\s+/)
|
96
|
-
return unless base_point = points.shift
|
97
|
-
x, y = base_point.split(",")
|
98
|
-
element.add_call 'move_to', x(x), y(y)
|
99
|
-
element.add_call_and_enter 'stroke'
|
100
|
-
points.each do |point|
|
101
|
-
x, y = point.split(",")
|
102
|
-
element.add_call "line_to", x(x), y(y)
|
103
|
-
end
|
104
|
-
|
105
|
-
when 'polygon'
|
106
|
-
points = attrs['points'].split(/\s+/).collect do |point|
|
107
|
-
x, y = point.split(",")
|
108
|
-
[x(x), y(y)]
|
109
|
-
end
|
110
|
-
element.add_call "polygon", *points
|
111
|
-
|
112
|
-
when 'circle'
|
113
|
-
if USE_NEW_CIRCLE_CALL
|
114
|
-
element.add_call "circle",
|
115
|
-
[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['r'])
|
116
|
-
else
|
117
|
-
element.add_call "circle_at",
|
118
|
-
[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], :radius => distance(attrs['r'])
|
119
|
-
end
|
120
|
-
|
121
|
-
when 'ellipse'
|
122
|
-
element.add_call USE_NEW_ELLIPSE_CALL ? "ellipse" : "ellipse_at",
|
123
|
-
[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])
|
124
|
-
|
125
|
-
when 'rect'
|
126
|
-
radius = distance(attrs['rx'] || attrs['ry'])
|
127
|
-
args = [[x(attrs['x'] || '0'), y(attrs['y'] || '0')], distance(attrs['width']), distance(attrs['height'])]
|
128
|
-
if radius
|
129
|
-
# n.b. does not support both rx and ry being specified with different values
|
130
|
-
element.add_call "rounded_rectangle", *(args + [radius])
|
131
|
-
else
|
132
|
-
element.add_call "rectangle", *args
|
133
|
-
end
|
134
|
-
|
135
|
-
when 'path'
|
136
|
-
parse_path(element)
|
137
|
-
|
138
|
-
when 'use'
|
139
|
-
parse_use(element)
|
140
|
-
|
141
|
-
when 'title', 'desc', 'metadata'
|
142
|
-
# ignore
|
143
|
-
do_not_append_calls = true
|
144
|
-
|
145
|
-
when 'font-face'
|
146
|
-
# not supported
|
147
|
-
do_not_append_calls = true
|
148
|
-
|
149
|
-
when 'image'
|
150
|
-
@svg_image ||= Image.new(@document)
|
151
|
-
@svg_image.parse(element)
|
152
|
-
|
153
|
-
else
|
154
|
-
@document.warnings << "Unknown tag '#{element.name}'; ignoring"
|
155
|
-
end
|
156
|
-
|
157
|
-
element.append_calls_to_parent unless do_not_append_calls
|
158
|
-
end
|
159
|
-
|
160
|
-
|
161
|
-
def parse_path(element)
|
162
|
-
@svg_path ||= Path.new
|
163
|
-
|
164
|
-
begin
|
165
|
-
commands = @svg_path.parse(element.attributes['d'])
|
166
|
-
rescue Prawn::Svg::Parser::Path::InvalidError => e
|
167
|
-
commands = []
|
168
|
-
@document.warnings << e.message
|
169
|
-
end
|
170
|
-
|
171
|
-
element.add_call 'join_style', :bevel
|
172
|
-
|
173
|
-
commands.collect do |command, args|
|
174
|
-
if args && args.length > 0
|
175
|
-
point_to = [x(args[0]), y(args[1])]
|
176
|
-
if command == 'curve_to'
|
177
|
-
opts = {:bounds => [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]}
|
178
|
-
end
|
179
|
-
element.add_call command, point_to, opts
|
180
|
-
else
|
181
|
-
element.add_call command
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
def parse_use(element)
|
187
|
-
if href = element.attributes['xlink:href']
|
188
|
-
if href[0..0] == '#'
|
189
|
-
id = href[1..-1]
|
190
|
-
if definition_element = @document.elements_by_id[id]
|
191
|
-
x = element.attributes['x']
|
192
|
-
y = element.attributes['y']
|
193
|
-
if x || y
|
194
|
-
element.add_call_and_enter "translate", distance(x || 0), -distance(y || 0)
|
195
|
-
end
|
196
|
-
element.add_calls_from_element definition_element
|
197
|
-
else
|
198
|
-
@document.warnings << "no tag with ID '#{id}' was found, referenced by use tag"
|
199
|
-
end
|
200
|
-
else
|
201
|
-
@document.warnings << "use tag has an href that is not a reference to an id; this is not supported"
|
202
|
-
end
|
203
|
-
else
|
204
|
-
@document.warnings << "no xlink:href specified on use tag"
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
####################################################################################################################
|
209
|
-
|
210
|
-
def load_css_styles(element)
|
211
|
-
if @document.css_parser
|
212
|
-
data = if element.element.cdatas.any?
|
213
|
-
element.element.cdatas.collect {|d| d.to_s}.join
|
214
|
-
else
|
215
|
-
element.element.text
|
216
|
-
end
|
217
|
-
|
218
|
-
@document.css_parser.add_block!(data)
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
def check_attrs_present(element, attrs)
|
223
|
-
missing_attrs = attrs - element.attributes.keys
|
224
|
-
if missing_attrs.any?
|
225
|
-
@document.warnings << "Must have attributes #{missing_attrs.join(", ")} on tag #{element.name}; skipping tag"
|
226
|
-
end
|
227
|
-
missing_attrs.empty?
|
228
|
-
end
|
229
|
-
|
230
|
-
%w(x y distance).each do |method|
|
231
|
-
define_method(method) {|*a| @document.send(method, *a)}
|
232
|
-
end
|
233
|
-
end
|