prawn-svg 0.21.0 → 0.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -0
  3. data/README.md +11 -4
  4. data/lib/prawn-svg.rb +9 -6
  5. data/lib/prawn/svg/attributes.rb +6 -0
  6. data/lib/prawn/svg/attributes/clip_path.rb +17 -0
  7. data/lib/prawn/svg/attributes/display.rb +5 -0
  8. data/lib/prawn/svg/attributes/font.rb +38 -0
  9. data/lib/prawn/svg/attributes/opacity.rb +15 -0
  10. data/lib/prawn/svg/attributes/stroke.rb +35 -0
  11. data/lib/prawn/svg/attributes/transform.rb +50 -0
  12. data/lib/prawn/svg/calculators/aspect_ratio.rb +1 -1
  13. data/lib/prawn/svg/calculators/document_sizing.rb +2 -2
  14. data/lib/prawn/svg/calculators/pixels.rb +1 -1
  15. data/lib/prawn/svg/color.rb +44 -14
  16. data/lib/prawn/svg/document.rb +6 -5
  17. data/lib/prawn/svg/elements.rb +33 -0
  18. data/lib/prawn/svg/elements/base.rb +228 -0
  19. data/lib/prawn/svg/elements/circle.rb +25 -0
  20. data/lib/prawn/svg/elements/container.rb +15 -0
  21. data/lib/prawn/svg/elements/ellipse.rb +23 -0
  22. data/lib/prawn/svg/elements/gradient.rb +117 -0
  23. data/lib/prawn/svg/elements/ignored.rb +5 -0
  24. data/lib/prawn/svg/elements/image.rb +85 -0
  25. data/lib/prawn/svg/elements/line.rb +16 -0
  26. data/lib/prawn/svg/elements/path.rb +405 -0
  27. data/lib/prawn/svg/elements/polygon.rb +17 -0
  28. data/lib/prawn/svg/elements/polyline.rb +22 -0
  29. data/lib/prawn/svg/elements/rect.rb +33 -0
  30. data/lib/prawn/svg/elements/root.rb +9 -0
  31. data/lib/prawn/svg/elements/style.rb +10 -0
  32. data/lib/prawn/svg/elements/text.rb +87 -0
  33. data/lib/prawn/svg/elements/use.rb +29 -0
  34. data/lib/prawn/svg/extension.rb +2 -2
  35. data/lib/prawn/svg/font.rb +3 -3
  36. data/lib/prawn/svg/interface.rb +12 -5
  37. data/lib/prawn/svg/url_loader.rb +1 -1
  38. data/lib/prawn/svg/version.rb +2 -2
  39. data/prawn-svg.gemspec +3 -3
  40. data/spec/integration_spec.rb +59 -2
  41. data/spec/prawn/svg/attributes/font_spec.rb +49 -0
  42. data/spec/prawn/svg/attributes/transform_spec.rb +56 -0
  43. data/spec/prawn/svg/calculators/aspect_ratio_spec.rb +2 -2
  44. data/spec/prawn/svg/calculators/document_sizing_spec.rb +3 -3
  45. data/spec/prawn/svg/color_spec.rb +36 -15
  46. data/spec/prawn/svg/document_spec.rb +4 -4
  47. data/spec/prawn/svg/elements/base_spec.rb +125 -0
  48. data/spec/prawn/svg/elements/gradient_spec.rb +61 -0
  49. data/spec/prawn/svg/elements/path_spec.rb +123 -0
  50. data/spec/prawn/svg/elements/style_spec.rb +23 -0
  51. data/spec/prawn/svg/{parser → elements}/text_spec.rb +7 -8
  52. data/spec/prawn/svg/font_spec.rb +12 -12
  53. data/spec/prawn/svg/interface_spec.rb +7 -7
  54. data/spec/prawn/svg/url_loader_spec.rb +2 -2
  55. data/spec/sample_svg/gradients.svg +40 -0
  56. data/spec/sample_svg/rect02.svg +8 -11
  57. data/spec/spec_helper.rb +1 -1
  58. metadata +46 -18
  59. data/lib/prawn/svg/element.rb +0 -304
  60. data/lib/prawn/svg/parser.rb +0 -268
  61. data/lib/prawn/svg/parser/image.rb +0 -81
  62. data/lib/prawn/svg/parser/path.rb +0 -392
  63. data/lib/prawn/svg/parser/text.rb +0 -80
  64. data/spec/prawn/svg/element_spec.rb +0 -127
  65. data/spec/prawn/svg/parser/path_spec.rb +0 -89
  66. data/spec/prawn/svg/parser_spec.rb +0 -55
@@ -0,0 +1,16 @@
1
+ class Prawn::SVG::Elements::Line < Prawn::SVG::Elements::Base
2
+ def parse
3
+ @x1 = x(attributes['x1'] || '0')
4
+ @y1 = y(attributes['y1'] || '0')
5
+ @x2 = x(attributes['x2'] || '0')
6
+ @y2 = y(attributes['y2'] || '0')
7
+ end
8
+
9
+ def apply
10
+ add_call 'line', @x1, @y1, @x2, @y2
11
+ end
12
+
13
+ def bounding_box
14
+ [@x1, @y1, @x2, @y2]
15
+ end
16
+ end
@@ -0,0 +1,405 @@
1
+ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
2
+ INSIDE_SPACE_REGEXP = /[ \t\r\n,]*/
3
+ OUTSIDE_SPACE_REGEXP = /[ \t\r\n]*/
4
+ INSIDE_REGEXP = /#{INSIDE_SPACE_REGEXP}([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:(?<=[0-9])e[+-]?[0-9]+)?)/
5
+ VALUES_REGEXP = /^#{INSIDE_REGEXP}/
6
+ COMMAND_REGEXP = /^#{OUTSIDE_SPACE_REGEXP}([A-Za-z])((?:#{INSIDE_REGEXP})*)#{OUTSIDE_SPACE_REGEXP}/
7
+
8
+ FLOAT_ERROR_DELTA = 1e-10
9
+
10
+ attr_reader :commands
11
+
12
+ def parse
13
+ require_attributes 'd'
14
+
15
+ @commands = []
16
+
17
+ data = attributes["d"].gsub(/#{OUTSIDE_SPACE_REGEXP}$/, '')
18
+
19
+ matched_commands = match_all(data, COMMAND_REGEXP)
20
+ raise SkipElementError, "Invalid/unsupported syntax for SVG path data" if matched_commands.nil?
21
+
22
+ matched_commands.each do |matched_command|
23
+ command = matched_command[1]
24
+ matched_values = match_all(matched_command[2], VALUES_REGEXP)
25
+ raise "should be impossible to have invalid inside data, but we ended up here" if matched_values.nil?
26
+ values = matched_values.collect {|value| value[1].to_f}
27
+ run_path_command(command, values)
28
+ end
29
+ end
30
+
31
+ def apply
32
+ add_call 'join_style', :bevel
33
+
34
+ @commands.collect do |command, args|
35
+ if args && args.length > 0
36
+ point_to = [x(args[0]), y(args[1])]
37
+ if command == 'curve_to'
38
+ opts = {:bounds => [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]}
39
+ end
40
+ add_call command, point_to, opts
41
+ else
42
+ add_call command
43
+ end
44
+ end
45
+ end
46
+
47
+ def bounding_box
48
+ x1, x2 = @commands.map {|_, args| x(args[0]) if args}.compact.minmax
49
+ y2, y1 = @commands.map {|_, args| y(args[1]) if args}.compact.minmax
50
+
51
+ [x1, y1, x2, y2]
52
+ end
53
+
54
+ protected
55
+
56
+ def run_path_command(command, values)
57
+ upcase_command = command.upcase
58
+ relative = command != upcase_command
59
+
60
+ case upcase_command
61
+ when 'M' # moveto
62
+ x = values.shift
63
+ y = values.shift
64
+
65
+ if relative && @last_point
66
+ x += @last_point.first
67
+ y += @last_point.last
68
+ end
69
+
70
+ @last_point = @subpath_initial_point = [x, y]
71
+ @commands << ["move_to", @last_point]
72
+
73
+ return run_path_command(relative ? 'l' : 'L', values) if values.any?
74
+
75
+ when 'Z' # closepath
76
+ if @subpath_initial_point
77
+ #@commands << ["line_to", @subpath_initial_point]
78
+ @commands << ["close_path"]
79
+ @last_point = @subpath_initial_point
80
+ end
81
+
82
+ when 'L' # lineto
83
+ while values.any?
84
+ x = values.shift
85
+ y = values.shift
86
+ if relative && @last_point
87
+ x += @last_point.first
88
+ y += @last_point.last
89
+ end
90
+ @last_point = [x, y]
91
+ @commands << ["line_to", @last_point]
92
+ end
93
+
94
+ when 'H' # horizontal lineto
95
+ while values.any?
96
+ x = values.shift
97
+ x += @last_point.first if relative && @last_point
98
+ @last_point = [x, @last_point.last]
99
+ @commands << ["line_to", @last_point]
100
+ end
101
+
102
+ when 'V' # vertical lineto
103
+ while values.any?
104
+ y = values.shift
105
+ y += @last_point.last if relative && @last_point
106
+ @last_point = [@last_point.first, y]
107
+ @commands << ["line_to", @last_point]
108
+ end
109
+
110
+ when 'C' # curveto
111
+ while values.any?
112
+ x1, y1, x2, y2, x, y = (1..6).collect {values.shift}
113
+ if relative && @last_point
114
+ x += @last_point.first
115
+ x1 += @last_point.first
116
+ x2 += @last_point.first
117
+ y += @last_point.last
118
+ y1 += @last_point.last
119
+ y2 += @last_point.last
120
+ end
121
+
122
+ @last_point = [x, y]
123
+ @previous_control_point = [x2, y2]
124
+ @commands << ["curve_to", [x, y, x1, y1, x2, y2]]
125
+ end
126
+
127
+ when 'S' # shorthand/smooth curveto
128
+ while values.any?
129
+ x2, y2, x, y = (1..4).collect {values.shift}
130
+ if relative && @last_point
131
+ x += @last_point.first
132
+ x2 += @last_point.first
133
+ y += @last_point.last
134
+ y2 += @last_point.last
135
+ end
136
+
137
+ if @previous_control_point
138
+ x1 = 2 * @last_point.first - @previous_control_point.first
139
+ y1 = 2 * @last_point.last - @previous_control_point.last
140
+ else
141
+ x1, y1 = @last_point
142
+ end
143
+
144
+ @last_point = [x, y]
145
+ @previous_control_point = [x2, y2]
146
+ @commands << ["curve_to", [x, y, x1, y1, x2, y2]]
147
+ end
148
+
149
+ when 'Q', 'T' # quadratic curveto
150
+ while values.any?
151
+ if shorthand = upcase_command == 'T'
152
+ x, y = (1..2).collect {values.shift}
153
+ else
154
+ x1, y1, x, y = (1..4).collect {values.shift}
155
+ end
156
+
157
+ if relative && @last_point
158
+ x += @last_point.first
159
+ x1 += @last_point.first if x1
160
+ y += @last_point.last
161
+ y1 += @last_point.last if y1
162
+ end
163
+
164
+ if shorthand
165
+ if @previous_quadratic_control_point
166
+ x1 = 2 * @last_point.first - @previous_quadratic_control_point.first
167
+ y1 = 2 * @last_point.last - @previous_quadratic_control_point.last
168
+ else
169
+ x1, y1 = @last_point
170
+ end
171
+ end
172
+
173
+ # convert from quadratic to cubic
174
+ cx1 = @last_point.first + (x1 - @last_point.first) * 2 / 3.0
175
+ cy1 = @last_point.last + (y1 - @last_point.last) * 2 / 3.0
176
+ cx2 = cx1 + (x - @last_point.first) / 3.0
177
+ cy2 = cy1 + (y - @last_point.last) / 3.0
178
+
179
+ @last_point = [x, y]
180
+ @previous_quadratic_control_point = [x1, y1]
181
+
182
+ @commands << ["curve_to", [x, y, cx1, cy1, cx2, cy2]]
183
+ end
184
+
185
+ when 'A'
186
+ return unless @last_point
187
+
188
+ while values.any?
189
+ rx, ry, phi, fa, fs, x2, y2 = (1..7).collect {values.shift}
190
+ x1, y1 = @last_point
191
+
192
+ return if rx.zero? && ry.zero?
193
+
194
+ if relative
195
+ x2 += x1
196
+ y2 += y1
197
+ end
198
+
199
+ # Normalise values as per F.6.2
200
+ rx = rx.abs
201
+ ry = ry.abs
202
+ phi = (phi % 360) * 2 * Math::PI / 360.0
203
+
204
+ # F.6.2: If the endpoints (x1, y1) and (x2, y2) are identical, then this is equivalent to omitting the elliptical arc segment entirely.
205
+ return if within_float_delta?(x1, x2) && within_float_delta?(y1, y2)
206
+
207
+ # F.6.2: If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a "lineto") joining the endpoints.
208
+ if within_float_delta?(rx, 0) || within_float_delta?(ry, 0)
209
+ @last_point = [x2, y2]
210
+ @commands << ["line_to", @last_point]
211
+ return
212
+ end
213
+
214
+ # We need to get the center co-ordinates, as well as the angles from the X axis to the start and end
215
+ # points. To do this, we use the algorithm documented in the SVG specification section F.6.5.
216
+
217
+ # F.6.5.1
218
+ xp1 = Math.cos(phi) * ((x1-x2)/2.0) + Math.sin(phi) * ((y1-y2)/2.0)
219
+ yp1 = -Math.sin(phi) * ((x1-x2)/2.0) + Math.cos(phi) * ((y1-y2)/2.0)
220
+
221
+ # F.6.6.2
222
+ r2x = rx * rx
223
+ r2y = ry * ry
224
+ hat = xp1 * xp1 / r2x + yp1 * yp1 / r2y
225
+ if hat > 1
226
+ rx *= Math.sqrt(hat)
227
+ ry *= Math.sqrt(hat)
228
+ end
229
+
230
+ # F.6.5.2
231
+ r2x = rx * rx
232
+ r2y = ry * ry
233
+ square = (r2x * r2y - r2x * yp1 * yp1 - r2y * xp1 * xp1) / (r2x * yp1 * yp1 + r2y * xp1 * xp1)
234
+ square = 0 if square < 0 && square > -FLOAT_ERROR_DELTA # catch rounding errors
235
+ base = Math.sqrt(square)
236
+ base *= -1 if fa == fs
237
+ cpx = base * rx * yp1 / ry
238
+ cpy = base * -ry * xp1 / rx
239
+
240
+ # F.6.5.3
241
+ cx = Math.cos(phi) * cpx + -Math.sin(phi) * cpy + (x1 + x2) / 2
242
+ cy = Math.sin(phi) * cpx + Math.cos(phi) * cpy + (y1 + y2) / 2
243
+
244
+ # F.6.5.5
245
+ vx = (xp1 - cpx) / rx
246
+ vy = (yp1 - cpy) / ry
247
+ theta_1 = Math.acos(vx / Math.sqrt(vx * vx + vy * vy))
248
+ theta_1 *= -1 if vy < 0
249
+
250
+ # F.6.5.6
251
+ ux = vx
252
+ uy = vy
253
+ vx = (-xp1 - cpx) / rx
254
+ vy = (-yp1 - cpy) / ry
255
+
256
+ numerator = ux * vx + uy * vy
257
+ denominator = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy)
258
+ division = numerator / denominator
259
+ division = -1 if division < -1 # for rounding errors
260
+
261
+ d_theta = Math.acos(division) % (2 * Math::PI)
262
+ d_theta *= -1 if ux * vy - uy * vx < 0
263
+
264
+ # Adjust range
265
+ if fs == 0
266
+ d_theta -= 2 * Math::PI if d_theta > 0
267
+ else
268
+ d_theta += 2 * Math::PI if d_theta < 0
269
+ end
270
+
271
+ theta_2 = theta_1 + d_theta
272
+
273
+ calculate_bezier_curve_points_for_arc(cx, cy, rx, ry, theta_1, theta_2, phi).each do |points|
274
+ @commands << ["curve_to", points[:p2] + points[:q1] + points[:q2]]
275
+ @last_point = points[:p2]
276
+ end
277
+ end
278
+ end
279
+
280
+ @previous_control_point = nil unless %w(C S).include?(upcase_command)
281
+ @previous_quadratic_control_point = nil unless %w(Q T).include?(upcase_command)
282
+ end
283
+
284
+ def within_float_delta?(a, b)
285
+ (a - b).abs < FLOAT_ERROR_DELTA
286
+ end
287
+
288
+ def match_all(string, regexp) # regexp must start with ^
289
+ result = []
290
+ while string != ""
291
+ matches = string.match(regexp)
292
+ result << matches
293
+ return if matches.nil?
294
+ string = matches.post_match
295
+ end
296
+ result
297
+ end
298
+
299
+ def calculate_eta_from_lambda(a, b, lambda_1, lambda_2)
300
+ # 2.2.1
301
+ eta1 = Math.atan2(Math.sin(lambda_1) / b, Math.cos(lambda_1) / a)
302
+ eta2 = Math.atan2(Math.sin(lambda_2) / b, Math.cos(lambda_2) / a)
303
+
304
+ # ensure eta1 <= eta2 <= eta1 + 2*PI
305
+ eta2 -= 2 * Math::PI * ((eta2 - eta1) / (2 * Math::PI)).floor
306
+ eta2 += 2 * Math::PI if lambda_2 - lambda_1 > Math::PI && eta2 - eta1 < Math::PI
307
+
308
+ [eta1, eta2]
309
+ end
310
+
311
+ # Convert the elliptical arc to a cubic bézier curve using this algorithm:
312
+ # http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
313
+ def calculate_bezier_curve_points_for_arc(cx, cy, a, b, lambda_1, lambda_2, theta)
314
+ e = lambda do |eta|
315
+ [
316
+ cx + a * Math.cos(theta) * Math.cos(eta) - b * Math.sin(theta) * Math.sin(eta),
317
+ cy + a * Math.sin(theta) * Math.cos(eta) + b * Math.cos(theta) * Math.sin(eta)
318
+ ]
319
+ end
320
+
321
+ ep = lambda do |eta|
322
+ [
323
+ -a * Math.cos(theta) * Math.sin(eta) - b * Math.sin(theta) * Math.cos(eta),
324
+ -a * Math.sin(theta) * Math.sin(eta) + b * Math.cos(theta) * Math.cos(eta)
325
+ ]
326
+ end
327
+
328
+ iterations = 1
329
+ d_lambda = lambda_2 - lambda_1
330
+
331
+ while iterations < 1024
332
+ if d_lambda.abs <= Math::PI / 2.0
333
+ # TODO : run error algorithm, see whether it meets threshold or not
334
+ # puts "error = #{calculate_curve_approximation_error(a, b, eta1, eta1 + d_eta)}"
335
+ break
336
+ end
337
+ iterations *= 2
338
+ d_lambda = (lambda_2 - lambda_1) / iterations
339
+ end
340
+
341
+ (0...iterations).collect do |iteration|
342
+ eta_a, eta_b = calculate_eta_from_lambda(a, b, lambda_1+iteration*d_lambda, lambda_1+(iteration+1)*d_lambda)
343
+ d_eta = eta_b - eta_a
344
+
345
+ alpha = Math.sin(d_eta) * ((Math.sqrt(4 + 3 * Math.tan(d_eta / 2) ** 2) - 1) / 3)
346
+
347
+ x1, y1 = e[eta_a]
348
+ x2, y2 = e[eta_b]
349
+
350
+ ep_eta1_x, ep_eta1_y = ep[eta_a]
351
+ q1_x = x1 + alpha * ep_eta1_x
352
+ q1_y = y1 + alpha * ep_eta1_y
353
+
354
+ ep_eta2_x, ep_eta2_y = ep[eta_b]
355
+ q2_x = x2 - alpha * ep_eta2_x
356
+ q2_y = y2 - alpha * ep_eta2_y
357
+
358
+ {:p2 => [x2, y2], :q1 => [q1_x, q1_y], :q2 => [q2_x, q2_y]}
359
+ end
360
+ end
361
+
362
+ ERROR_COEFFICIENTS_A = [
363
+ [
364
+ [3.85268, -21.229, -0.330434, 0.0127842],
365
+ [-1.61486, 0.706564, 0.225945, 0.263682],
366
+ [-0.910164, 0.388383, 0.00551445, 0.00671814],
367
+ [-0.630184, 0.192402, 0.0098871, 0.0102527]
368
+ ],
369
+ [
370
+ [-0.162211, 9.94329, 0.13723, 0.0124084],
371
+ [-0.253135, 0.00187735, 0.0230286, 0.01264],
372
+ [-0.0695069, -0.0437594, 0.0120636, 0.0163087],
373
+ [-0.0328856, -0.00926032, -0.00173573, 0.00527385]
374
+ ]
375
+ ]
376
+
377
+ ERROR_COEFFICIENTS_B = [
378
+ [
379
+ [0.0899116, -19.2349, -4.11711, 0.183362],
380
+ [0.138148, -1.45804, 1.32044, 1.38474],
381
+ [0.230903, -0.450262, 0.219963, 0.414038],
382
+ [0.0590565, -0.101062, 0.0430592, 0.0204699]
383
+ ],
384
+ [
385
+ [0.0164649, 9.89394, 0.0919496, 0.00760802],
386
+ [0.0191603, -0.0322058, 0.0134667, -0.0825018],
387
+ [0.0156192, -0.017535, 0.00326508, -0.228157],
388
+ [-0.0236752, 0.0405821, -0.0173086, 0.176187]
389
+ ]
390
+ ]
391
+
392
+ def calculate_curve_approximation_error(a, b, eta1, eta2)
393
+ b_over_a = b / a
394
+ coefficents = b_over_a < 0.25 ? ERROR_COEFFICIENTS_A : ERROR_COEFFICIENTS_B
395
+
396
+ c = lambda do |i|
397
+ (0..3).inject(0) do |accumulator, j|
398
+ coef = coefficents[i][j]
399
+ accumulator + ((coef[0] * b_over_a**2 + coef[1] * b_over_a + coef[2]) / (b_over_a * coef[3])) * Math.cos(j * (eta1 + eta2))
400
+ end
401
+ end
402
+
403
+ ((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))
404
+ end
405
+ end
@@ -0,0 +1,17 @@
1
+ class Prawn::SVG::Elements::Polygon < Prawn::SVG::Elements::Base
2
+ def parse
3
+ require_attributes('points')
4
+ @points = parse_points(attributes['points'])
5
+ end
6
+
7
+ def apply
8
+ add_call 'polygon', *@points
9
+ end
10
+
11
+ def bounding_box
12
+ x1, x2 = @points.map(&:first).minmax
13
+ y2, y1 = @points.map(&:last).minmax
14
+ [x1, y1, x2, y2]
15
+ end
16
+ end
17
+
@@ -0,0 +1,22 @@
1
+ class Prawn::SVG::Elements::Polyline < Prawn::SVG::Elements::Base
2
+ def parse
3
+ require_attributes('points')
4
+ @points = parse_points(attributes['points'])
5
+ end
6
+
7
+ def apply
8
+ raise SkipElementQuietly unless @points.length > 0
9
+
10
+ add_call 'move_to', *@points[0]
11
+ add_call_and_enter 'stroke'
12
+ @points[1..-1].each do |x, y|
13
+ add_call "line_to", x, y
14
+ end
15
+ end
16
+
17
+ def bounding_box
18
+ x1, x2 = @points.map(&:first).minmax
19
+ y2, y1 = @points.map(&:last).minmax
20
+ [x1, y1, x2, y2]
21
+ end
22
+ end