prawn-svg 0.21.0 → 0.22.1

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.
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