prawn-svg 0.23.1 → 0.24.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.
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
-