prawn-svg 0.23.1 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -3
  3. data/README.md +15 -9
  4. data/lib/prawn-svg.rb +3 -0
  5. data/lib/prawn/svg/attributes.rb +1 -1
  6. data/lib/prawn/svg/attributes/clip_path.rb +6 -7
  7. data/lib/prawn/svg/attributes/opacity.rb +3 -3
  8. data/lib/prawn/svg/attributes/stroke.rb +6 -4
  9. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +114 -0
  10. data/lib/prawn/svg/elements.rb +4 -1
  11. data/lib/prawn/svg/elements/base.rb +76 -69
  12. data/lib/prawn/svg/elements/container.rb +5 -6
  13. data/lib/prawn/svg/elements/gradient.rb +4 -4
  14. data/lib/prawn/svg/elements/image.rb +1 -1
  15. data/lib/prawn/svg/elements/line.rb +15 -7
  16. data/lib/prawn/svg/elements/marker.rb +72 -0
  17. data/lib/prawn/svg/elements/path.rb +23 -147
  18. data/lib/prawn/svg/elements/polygon.rb +14 -6
  19. data/lib/prawn/svg/elements/polyline.rb +12 -11
  20. data/lib/prawn/svg/elements/root.rb +3 -1
  21. data/lib/prawn/svg/elements/text.rb +38 -17
  22. data/lib/prawn/svg/font.rb +6 -6
  23. data/lib/prawn/svg/interface.rb +3 -0
  24. data/lib/prawn/svg/pathable.rb +130 -0
  25. data/lib/prawn/svg/properties.rb +122 -0
  26. data/lib/prawn/svg/state.rb +7 -29
  27. data/lib/prawn/svg/version.rb +1 -1
  28. data/spec/prawn/svg/elements/base_spec.rb +19 -32
  29. data/spec/prawn/svg/elements/line_spec.rb +37 -0
  30. data/spec/prawn/svg/elements/marker_spec.rb +90 -0
  31. data/spec/prawn/svg/elements/path_spec.rb +10 -10
  32. data/spec/prawn/svg/elements/polygon_spec.rb +49 -0
  33. data/spec/prawn/svg/elements/polyline_spec.rb +47 -0
  34. data/spec/prawn/svg/elements/style_spec.rb +1 -1
  35. data/spec/prawn/svg/elements/text_spec.rb +37 -5
  36. data/spec/prawn/svg/pathable_spec.rb +92 -0
  37. data/spec/prawn/svg/properties_spec.rb +186 -0
  38. data/spec/sample_svg/arrows.svg +73 -0
  39. data/spec/sample_svg/marker.svg +32 -0
  40. data/spec/sample_svg/polygon01.svg +25 -5
  41. metadata +23 -8
  42. data/lib/prawn/svg/attributes/color.rb +0 -5
  43. data/lib/prawn/svg/attributes/display.rb +0 -5
  44. data/lib/prawn/svg/attributes/font.rb +0 -38
  45. data/spec/prawn/svg/attributes/font_spec.rb +0 -52
@@ -1,12 +1,11 @@
1
1
  class Prawn::SVG::Elements::Container < Prawn::SVG::Elements::Base
2
2
  def parse
3
- state.disable_drawing = true if name == "clipPath"
4
- end
5
-
6
- def apply
7
- process_child_elements
3
+ state.disable_drawing = true if name == 'clipPath'
8
4
 
9
- raise SkipElementQuietly if %w(symbol defs clipPath).include?(name)
5
+ if %w(symbol defs clipPath).include?(name)
6
+ properties.display = 'none'
7
+ computed_properties.display = 'none'
8
+ end
10
9
  end
11
10
 
12
11
  def container?
@@ -51,16 +51,16 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
51
51
  end
52
52
 
53
53
  def load_gradient_configuration
54
- @units = attributes["gradientunits"] == 'userSpaceOnUse' ? :user_space : :bounding_box
54
+ @units = attributes["gradientUnits"] == 'userSpaceOnUse' ? :user_space : :bounding_box
55
55
 
56
- if transform = attributes["gradienttransform"]
56
+ if transform = attributes["gradientTransform"]
57
57
  matrix = transform.split(COMMA_WSP_REGEXP).map(&:to_f)
58
58
  if matrix != [1, 0, 0, 1, 0, 0]
59
59
  raise SkipElementError, "prawn-svg does not yet support gradients with a non-identity gradientTransform attribute"
60
60
  end
61
61
  end
62
62
 
63
- if (spread_method = attributes['spreadmethod']) && spread_method != "pad"
63
+ if (spread_method = attributes['spreadMethod']) && spread_method != "pad"
64
64
  warnings << "prawn-svg only currently supports the 'pad' spreadMethod attribute value"
65
65
  end
66
66
  end
@@ -98,7 +98,7 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
98
98
  offset = result.last.first
99
99
  end
100
100
 
101
- if color_hex = Prawn::SVG::Color.color_to_hex(child.attributes["stop-color"])
101
+ if color_hex = Prawn::SVG::Color.color_to_hex(child.properties.stop_color)
102
102
  result << [offset, color_hex]
103
103
  end
104
104
  end
@@ -13,7 +13,7 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
13
13
  def parse
14
14
  require_attributes 'width', 'height'
15
15
 
16
- raise SkipElementQuietly if state.display == "none"
16
+ raise SkipElementQuietly if state.computed_properties.display == "none"
17
17
 
18
18
  @url = attributes['xlink:href'] || attributes['href']
19
19
  if @url.nil?
@@ -1,16 +1,24 @@
1
1
  class Prawn::SVG::Elements::Line < Prawn::SVG::Elements::Base
2
+ include Prawn::SVG::Pathable
3
+
2
4
  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')
5
+ @x1 = points(attributes['x1'] || '0', :x)
6
+ @y1 = points(attributes['y1'] || '0', :y)
7
+ @x2 = points(attributes['x2'] || '0', :x)
8
+ @y2 = points(attributes['y2'] || '0', :y)
7
9
  end
8
10
 
9
11
  def apply
10
- add_call 'line', @x1, @y1, @x2, @y2
12
+ apply_commands
13
+ apply_markers
11
14
  end
12
15
 
13
- def bounding_box
14
- [@x1, @y1, @x2, @y2]
16
+ protected
17
+
18
+ def commands
19
+ @commands ||= [
20
+ Prawn::SVG::Pathable::Move.new([@x1, @y1]),
21
+ Prawn::SVG::Pathable::Line.new([@x2, @y2])
22
+ ]
15
23
  end
16
24
  end
@@ -0,0 +1,72 @@
1
+ class Prawn::SVG::Elements::Marker < Prawn::SVG::Elements::Base
2
+ def parse
3
+ properties.display = 'none'
4
+ computed_properties.display = 'none'
5
+ end
6
+
7
+ def container?
8
+ true
9
+ end
10
+
11
+ def apply_marker(element, point: nil, angle: 0)
12
+ sizing = Prawn::SVG::Calculators::DocumentSizing.new([0, 0], attributes)
13
+ sizing.document_width = attributes["markerWidth"] || 3
14
+ sizing.document_height = attributes["markerHeight"] || 3
15
+ sizing.calculate
16
+
17
+ if sizing.invalid?
18
+ document.warnings << "<marker> cannot be rendered due to invalid sizing information"
19
+ return
20
+ end
21
+
22
+ element.new_call_context_from_base do
23
+ element.add_call 'save'
24
+
25
+ # LATER : these will probably change when we separate out properties from attributes
26
+ element.parse_transform_attribute_and_call
27
+ element.parse_opacity_attributes_and_call
28
+ element.parse_clip_path_attribute_and_call
29
+
30
+ element.add_call 'transformation_matrix', 1, 0, 0, 1, point[0], -point[1]
31
+
32
+ if attributes['orient'] != 'auto'
33
+ angle = attributes['orient'].to_f # defaults to 0 if not specified
34
+ end
35
+
36
+ element.push_call_position
37
+ element.add_call_and_enter 'rotate', -angle, origin: [0, y('0')] if angle != 0
38
+
39
+ if attributes['markerUnits'] != 'userSpaceOnUse'
40
+ scale = element.state.stroke_width
41
+ element.add_call 'transformation_matrix', scale, 0, 0, scale, 0, 0
42
+ end
43
+
44
+ ref_x = document.distance(attributes['refX']) || 0
45
+ ref_y = document.distance(attributes['refY']) || 0
46
+
47
+ element.add_call 'transformation_matrix', 1, 0, 0, 1, -ref_x * sizing.x_scale, ref_y * sizing.y_scale
48
+
49
+ # `overflow: visible` must be on the <marker> element
50
+ if properties.overflow != 'visible'
51
+ point = [sizing.x_offset * sizing.x_scale, y(sizing.y_offset * sizing.y_scale)]
52
+ element.add_call "rectangle", point, sizing.output_width, sizing.output_height
53
+ element.add_call "clip"
54
+ end
55
+
56
+ element.add_call 'transformation_matrix', sizing.x_scale, 0, 0, sizing.y_scale, 0, 0
57
+
58
+ new_state = state.dup
59
+ new_state.computed_properties = computed_properties.dup
60
+
61
+ container = Prawn::SVG::Elements::Container.new(document, nil, [], new_state)
62
+ container.properties.compute_properties(new_state.computed_properties)
63
+ container.parse_and_apply
64
+ container.add_calls_from_element(self)
65
+
66
+ element.add_calls_from_element(container)
67
+
68
+ element.pop_call_position
69
+ element.add_call 'restore'
70
+ end
71
+ end
72
+ end
@@ -1,4 +1,7 @@
1
1
  class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
2
+ include Prawn::SVG::Calculators::ArcToBezierCurve
3
+ include Prawn::SVG::Pathable
4
+
2
5
  INSIDE_SPACE_REGEXP = /[ \t\r\n,]*/
3
6
  OUTSIDE_SPACE_REGEXP = /[ \t\r\n]*/
4
7
  INSIDE_REGEXP = /#{INSIDE_SPACE_REGEXP}([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:(?<=[0-9])e[+-]?[0-9]+)?)/
@@ -24,36 +27,20 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
24
27
  matched_values = match_all(matched_command[2], VALUES_REGEXP)
25
28
  raise "should be impossible to have invalid inside data, but we ended up here" if matched_values.nil?
26
29
  values = matched_values.collect {|value| value[1].to_f}
27
- run_path_command(command, values)
30
+ parse_path_command(command, values)
28
31
  end
29
32
  end
30
33
 
31
34
  def apply
32
35
  add_call 'join_style', :bevel
33
36
 
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]
37
+ apply_commands
38
+ apply_markers
52
39
  end
53
40
 
54
41
  protected
55
42
 
56
- def run_path_command(command, values)
43
+ def parse_path_command(command, values)
57
44
  upcase_command = command.upcase
58
45
  relative = command != upcase_command
59
46
 
@@ -67,16 +54,14 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
67
54
  y += @last_point.last
68
55
  end
69
56
 
70
- @last_point = @subpath_initial_point = [x, y]
71
- @commands << ["move_to", @last_point]
57
+ @subpath_initial_point = [x, y]
58
+ push_command Prawn::SVG::Pathable::Move.new(@subpath_initial_point)
72
59
 
73
- return run_path_command(relative ? 'l' : 'L', values) if values.any?
60
+ return parse_path_command(relative ? 'l' : 'L', values) if values.any?
74
61
 
75
62
  when 'Z' # closepath
76
63
  if @subpath_initial_point
77
- #@commands << ["line_to", @subpath_initial_point]
78
- @commands << ["close_path"]
79
- @last_point = @subpath_initial_point
64
+ push_command Prawn::SVG::Pathable::Close.new(@subpath_initial_point)
80
65
  end
81
66
 
82
67
  when 'L' # lineto
@@ -87,24 +72,22 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
87
72
  x += @last_point.first
88
73
  y += @last_point.last
89
74
  end
90
- @last_point = [x, y]
91
- @commands << ["line_to", @last_point]
75
+
76
+ push_command Prawn::SVG::Pathable::Line.new([x, y])
92
77
  end
93
78
 
94
79
  when 'H' # horizontal lineto
95
80
  while values.any?
96
81
  x = values.shift
97
82
  x += @last_point.first if relative && @last_point
98
- @last_point = [x, @last_point.last]
99
- @commands << ["line_to", @last_point]
83
+ push_command Prawn::SVG::Pathable::Line.new([x, @last_point.last])
100
84
  end
101
85
 
102
86
  when 'V' # vertical lineto
103
87
  while values.any?
104
88
  y = values.shift
105
89
  y += @last_point.last if relative && @last_point
106
- @last_point = [@last_point.first, y]
107
- @commands << ["line_to", @last_point]
90
+ push_command Prawn::SVG::Pathable::Line.new([@last_point.first, y])
108
91
  end
109
92
 
110
93
  when 'C' # curveto
@@ -119,9 +102,8 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
119
102
  y2 += @last_point.last
120
103
  end
121
104
 
122
- @last_point = [x, y]
123
105
  @previous_control_point = [x2, y2]
124
- @commands << ["curve_to", [x, y, x1, y1, x2, y2]]
106
+ push_command Prawn::SVG::Pathable::Curve.new([x, y], [x1, y1], [x2, y2])
125
107
  end
126
108
 
127
109
  when 'S' # shorthand/smooth curveto
@@ -141,9 +123,8 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
141
123
  x1, y1 = @last_point
142
124
  end
143
125
 
144
- @last_point = [x, y]
145
126
  @previous_control_point = [x2, y2]
146
- @commands << ["curve_to", [x, y, x1, y1, x2, y2]]
127
+ push_command Prawn::SVG::Pathable::Curve.new([x, y], [x1, y1], [x2, y2])
147
128
  end
148
129
 
149
130
  when 'Q', 'T' # quadratic curveto
@@ -176,10 +157,9 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
176
157
  cx2 = cx1 + (x - @last_point.first) / 3.0
177
158
  cy2 = cy1 + (y - @last_point.last) / 3.0
178
159
 
179
- @last_point = [x, y]
180
160
  @previous_quadratic_control_point = [x1, y1]
181
161
 
182
- @commands << ["curve_to", [x, y, cx1, cy1, cx2, cy2]]
162
+ push_command Prawn::SVG::Pathable::Curve.new([x, y], [cx1, cy1], [cx2, cy2])
183
163
  end
184
164
 
185
165
  when 'A'
@@ -206,8 +186,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
206
186
 
207
187
  # 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
188
  if within_float_delta?(rx, 0) || within_float_delta?(ry, 0)
209
- @last_point = [x2, y2]
210
- @commands << ["line_to", @last_point]
189
+ push_command Prawn::SVG::Pathable::Line.new([x2, y2])
211
190
  return
212
191
  end
213
192
 
@@ -271,8 +250,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
271
250
  theta_2 = theta_1 + d_theta
272
251
 
273
252
  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]
253
+ push_command Prawn::SVG::Pathable::Curve.new(points[:p2], points[:q1], points[:q2])
276
254
  end
277
255
  end
278
256
  end
@@ -296,110 +274,8 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
296
274
  result
297
275
  end
298
276
 
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))
277
+ def push_command(command)
278
+ @commands << command
279
+ @last_point = command.destination
404
280
  end
405
281
  end
@@ -1,17 +1,25 @@
1
1
  class Prawn::SVG::Elements::Polygon < Prawn::SVG::Elements::Base
2
+ include Prawn::SVG::Pathable
3
+
2
4
  def parse
3
5
  require_attributes('points')
4
6
  @points = parse_points(attributes['points'])
5
7
  end
6
8
 
7
9
  def apply
8
- add_call 'polygon', *@points
10
+ apply_commands
11
+ apply_markers
9
12
  end
10
13
 
11
- def bounding_box
12
- x1, x2 = @points.map(&:first).minmax
13
- y2, y1 = @points.map(&:last).minmax
14
- [x1, y1, x2, y2]
14
+ protected
15
+
16
+ def commands
17
+ @commands ||= [
18
+ Prawn::SVG::Pathable::Move.new(@points[0])
19
+ ] + @points[1..-1].map { |point|
20
+ Prawn::SVG::Pathable::Line.new(point)
21
+ } + [
22
+ Prawn::SVG::Pathable::Close.new(@points[0])
23
+ ]
15
24
  end
16
25
  end
17
-