prawn-svg 0.29.1 → 0.32.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +18 -0
  3. data/LICENSE +1 -1
  4. data/README.md +10 -9
  5. data/lib/prawn/svg/attributes/opacity.rb +4 -4
  6. data/lib/prawn/svg/attributes/transform.rb +2 -44
  7. data/lib/prawn/svg/elements/base.rb +11 -5
  8. data/lib/prawn/svg/elements/call_duplicator.rb +1 -1
  9. data/lib/prawn/svg/elements/gradient.rb +83 -25
  10. data/lib/prawn/svg/elements/image.rb +2 -2
  11. data/lib/prawn/svg/elements/path.rb +42 -29
  12. data/lib/prawn/svg/elements/root.rb +4 -1
  13. data/lib/prawn/svg/elements/text_component.rb +21 -5
  14. data/lib/prawn/svg/elements/use.rb +23 -7
  15. data/lib/prawn/svg/elements.rb +2 -0
  16. data/lib/prawn/svg/extensions/additional_gradient_transforms.rb +23 -0
  17. data/lib/prawn/svg/interface.rb +38 -8
  18. data/lib/prawn/svg/loaders/data.rb +1 -1
  19. data/lib/prawn/svg/loaders/file.rb +3 -1
  20. data/lib/prawn/svg/properties.rb +1 -0
  21. data/lib/prawn/svg/transform_parser.rb +72 -0
  22. data/lib/prawn/svg/version.rb +1 -1
  23. data/lib/prawn-svg.rb +4 -0
  24. data/prawn-svg.gemspec +3 -2
  25. data/spec/integration_spec.rb +24 -24
  26. data/spec/prawn/svg/attributes/opacity_spec.rb +85 -0
  27. data/spec/prawn/svg/attributes/transform_spec.rb +30 -35
  28. data/spec/prawn/svg/css/stylesheets_spec.rb +1 -19
  29. data/spec/prawn/svg/elements/base_spec.rb +16 -16
  30. data/spec/prawn/svg/elements/gradient_spec.rb +79 -4
  31. data/spec/prawn/svg/elements/line_spec.rb +12 -12
  32. data/spec/prawn/svg/elements/marker_spec.rb +27 -27
  33. data/spec/prawn/svg/elements/path_spec.rb +29 -17
  34. data/spec/prawn/svg/elements/polygon_spec.rb +9 -9
  35. data/spec/prawn/svg/elements/polyline_spec.rb +7 -7
  36. data/spec/prawn/svg/elements/text_spec.rb +65 -50
  37. data/spec/prawn/svg/loaders/data_spec.rb +8 -0
  38. data/spec/prawn/svg/pathable_spec.rb +4 -4
  39. data/spec/prawn/svg/transform_parser_spec.rb +94 -0
  40. data/spec/sample_svg/double_opacity.svg +6 -0
  41. data/spec/sample_svg/gradient_transform.svg +19 -0
  42. data/spec/sample_svg/links.svg +18 -0
  43. data/spec/sample_svg/radgrad01-bounding.svg +26 -0
  44. data/spec/sample_svg/radgrad01.svg +26 -0
  45. data/spec/sample_svg/svg_fill.svg +5 -0
  46. data/spec/sample_svg/text-decoration.svg +4 -0
  47. data/spec/sample_svg/transform.svg +20 -0
  48. data/spec/sample_svg/use_disordered.svg +17 -0
  49. data/spec/spec_helper.rb +2 -2
  50. metadata +48 -10
  51. data/.travis.yml +0 -8
@@ -1,19 +1,35 @@
1
1
  class Prawn::SVG::Elements::Use < Prawn::SVG::Elements::Base
2
- attr_reader :referenced_element
2
+ attr_reader :referenced_element_class
3
+ attr_reader :referenced_element_source
3
4
 
4
5
  def parse
5
- require_attributes 'xlink:href'
6
-
7
- href = attributes['xlink:href']
6
+ href = href_attribute
7
+ if href.nil?
8
+ raise SkipElementError, "use tag must have an href or xlink:href"
9
+ end
8
10
 
9
11
  if href[0..0] != '#'
10
12
  raise SkipElementError, "use tag has an href that is not a reference to an id; this is not supported"
11
13
  end
12
14
 
13
15
  id = href[1..-1]
14
- @referenced_element = @document.elements_by_id[id]
16
+ referenced_element = @document.elements_by_id[id]
17
+
18
+ if referenced_element
19
+ @referenced_element_class = referenced_element.class
20
+ @referenced_element_source = referenced_element.source
21
+ else
22
+ # Perhaps the element is defined further down in the document. This is not recommended but still valid SVG,
23
+ # so we'll support it with an exception case that's not particularly performant.
24
+ raw_element = REXML::XPath.match(@document.root, %(//*[@id="#{id.gsub('"', '\"')}"])).first
25
+
26
+ if raw_element
27
+ @referenced_element_class = Prawn::SVG::Elements::TAG_CLASS_MAPPING[raw_element.name.to_sym]
28
+ @referenced_element_source = raw_element
29
+ end
30
+ end
15
31
 
16
- if referenced_element.nil?
32
+ if referenced_element_class.nil?
17
33
  raise SkipElementError, "no tag with ID '#{id}' was found, referenced by use tag"
18
34
  end
19
35
 
@@ -36,7 +52,7 @@ class Prawn::SVG::Elements::Use < Prawn::SVG::Elements::Base
36
52
  def process_child_elements
37
53
  add_call "save"
38
54
 
39
- child = referenced_element.class.new(referenced_element.document, referenced_element.source, calls, state.dup)
55
+ child = referenced_element_class.new(document, referenced_element_source, calls, state.dup)
40
56
  child.process
41
57
 
42
58
  add_call "restore"
@@ -13,6 +13,7 @@ module Prawn::SVG::Elements
13
13
  g: Prawn::SVG::Elements::Container,
14
14
  symbol: Prawn::SVG::Elements::Container,
15
15
  defs: Prawn::SVG::Elements::Container,
16
+ a: Prawn::SVG::Elements::Container,
16
17
  clipPath: Prawn::SVG::Elements::ClipPath,
17
18
  switch: Prawn::SVG::Elements::Container,
18
19
  svg: Prawn::SVG::Elements::Viewport,
@@ -27,6 +28,7 @@ module Prawn::SVG::Elements
27
28
  use: Prawn::SVG::Elements::Use,
28
29
  image: Prawn::SVG::Elements::Image,
29
30
  linearGradient: Prawn::SVG::Elements::Gradient,
31
+ radialGradient: Prawn::SVG::Elements::Gradient,
30
32
  marker: Prawn::SVG::Elements::Marker,
31
33
  style: Prawn::SVG::Elements::Ignored, # because it is pre-parsed by Document
32
34
  title: Prawn::SVG::Elements::Ignored,
@@ -0,0 +1,23 @@
1
+ module Prawn::SVG::Extensions
2
+ module AdditionalGradientTransforms
3
+ def gradient_coordinates(gradient)
4
+ # As of Prawn 2.2.0, apply_transformations is used as purely a boolean.
5
+ #
6
+ # Here we're using it to optionally pass in a 6-tuple transformation matrix that gets applied to the
7
+ # gradient. This should be added to Prawn properly, and then this monkey patch will not be necessary.
8
+
9
+ if gradient.apply_transformations.is_a?(Array)
10
+ x1, y1, x2, y2, transformation = super
11
+ a, b, c, d, e, f = transformation
12
+ na, nb, nc, nd, ne, nf = gradient.apply_transformations
13
+
14
+ matrix = Matrix[[a, c, e], [b, d, f], [0, 0, 1]] * Matrix[[na, nc, ne], [nb, nd, nf], [0, 0, 1]]
15
+ new_transformation = matrix.to_a[0..1].transpose.flatten
16
+
17
+ [x1, y1, x2, y2, new_transformation]
18
+ else
19
+ super
20
+ end
21
+ end
22
+ end
23
+ end
@@ -109,32 +109,40 @@ module Prawn
109
109
  end
110
110
 
111
111
  def issue_prawn_command(prawn, calls)
112
- calls.each do |call, arguments, children|
112
+ calls.each do |call, arguments, kwarguments, children|
113
113
  skip = false
114
114
 
115
- rewrite_call_arguments(prawn, call, arguments) do
115
+ rewrite_call_arguments(prawn, call, arguments, kwarguments) do
116
116
  issue_prawn_command(prawn, children) if children.any?
117
117
  skip = true
118
118
  end
119
119
 
120
120
  if skip
121
121
  # the call has been overridden
122
- elsif children.empty?
123
- prawn.send(call, *arguments)
122
+ elsif children.empty? && call != 'transparent' # some prawn calls complain if they aren't supplied a block
123
+ if RUBY_VERSION >= '2.7' || !kwarguments.empty?
124
+ prawn.send(call, *arguments, **kwarguments)
125
+ else
126
+ prawn.send(call, *arguments)
127
+ end
124
128
  else
125
- prawn.send(call, *arguments, &proc_creator(prawn, children))
129
+ if RUBY_VERSION >= '2.7' || !kwarguments.empty?
130
+ prawn.send(call, *arguments, **kwarguments, &proc_creator(prawn, children))
131
+ else
132
+ prawn.send(call, *arguments, &proc_creator(prawn, children))
133
+ end
126
134
  end
127
135
  end
128
136
  end
129
137
 
130
- def rewrite_call_arguments(prawn, call, arguments)
138
+ def rewrite_call_arguments(prawn, call, arguments, kwarguments)
131
139
  case call
132
140
  when 'text_group'
133
141
  @cursor = [0, document.sizing.output_height]
134
142
  yield
135
143
 
136
144
  when 'draw_text'
137
- text, options = arguments
145
+ text, options = arguments.first, kwarguments
138
146
 
139
147
  at = options.fetch(:at)
140
148
 
@@ -148,6 +156,19 @@ module Prawn
148
156
 
149
157
  width = prawn.width_of(text, options.merge(kerning: true))
150
158
 
159
+ if stretch_to_width = options.delete(:stretch_to_width)
160
+ factor = stretch_to_width.to_f * 100 / width.to_f
161
+ prawn.add_content "#{factor} Tz"
162
+ width = stretch_to_width.to_f
163
+ end
164
+
165
+ if pad_to_width = options.delete(:pad_to_width)
166
+ padding_required = pad_to_width.to_f - width.to_f
167
+ padding_per_character = padding_required / text.length.to_f
168
+ prawn.add_content "#{padding_per_character} Tc"
169
+ width = pad_to_width.to_f
170
+ end
171
+
151
172
  case options.delete(:text_anchor)
152
173
  when 'middle'
153
174
  at[0] -= width / 2
@@ -159,6 +180,15 @@ module Prawn
159
180
  @cursor = [at[0] + width, at[1]]
160
181
  end
161
182
 
183
+ decoration = options.delete(:decoration)
184
+ if decoration == 'underline'
185
+ prawn.save_graphics_state do
186
+ prawn.line_width 1
187
+ prawn.line [at[0], at[1] - 1.25], [at[0] + width, at[1] - 1.25]
188
+ prawn.stroke
189
+ end
190
+ end
191
+
162
192
  when 'transformation_matrix'
163
193
  left = prawn.bounds.absolute_left
164
194
  top = prawn.bounds.absolute_top
@@ -186,7 +216,7 @@ module Prawn
186
216
  # prawn (as at 2.0.1 anyway) uses 'b' for its fill_and_stroke. 'b' is 'h' (closepath) + 'B', and we
187
217
  # never want closepath to be automatically run as it stuffs up many drawing operations, such as dashes
188
218
  # and line caps, and makes paths close that we didn't ask to be closed when fill is specified.
189
- even_odd = arguments[0].is_a?(Hash) && arguments[0][:fill_rule] == :even_odd
219
+ even_odd = kwarguments[:fill_rule] == :even_odd
190
220
  content = even_odd ? 'B*' : 'B'
191
221
  prawn.add_content content
192
222
 
@@ -2,7 +2,7 @@ require 'base64'
2
2
 
3
3
  module Prawn::SVG::Loaders
4
4
  class Data
5
- REGEXP = %r[\A(?i:data):image/(png|jpeg);base64(;[a-z0-9]+)*,]
5
+ REGEXP = %r[\Adata:image/(png|jpeg);base64(;[a-z0-9]+)*,]i
6
6
 
7
7
  def from_url(url)
8
8
  return if url[0..4].downcase != "data:"
@@ -1,3 +1,5 @@
1
+ require 'addressable/uri'
2
+
1
3
  #
2
4
  # Load a file from disk.
3
5
  #
@@ -56,7 +58,7 @@ module Prawn::SVG::Loaders
56
58
  private
57
59
 
58
60
  def load_file(path)
59
- path = URI.decode(path)
61
+ path = Addressable::URI.unencode(path)
60
62
  path = build_absolute_and_expand_path(path)
61
63
  assert_valid_path!(path)
62
64
  assert_file_exists!(path)
@@ -37,6 +37,7 @@ class Prawn::SVG::Properties
37
37
  "stroke-opacity" => Config.new("1", true),
38
38
  "stroke-width" => Config.new("1", true),
39
39
  "text-anchor" => Config.new("start", true, %w(inherit start middle end), true),
40
+ 'text-decoration' => Config.new('none', true, %w(inherit none underline), true),
40
41
  }.freeze
41
42
 
42
43
  PROPERTIES.each do |name, value|
@@ -0,0 +1,72 @@
1
+ module Prawn::SVG::TransformParser
2
+ def parse_transform_attribute(transform)
3
+ matrix = Matrix.identity(3)
4
+
5
+ parse_css_method_calls(transform).each do |name, arguments|
6
+ case name
7
+ when 'translate'
8
+ x, y = arguments
9
+ matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]
10
+
11
+ when 'translateX'
12
+ x = arguments.first
13
+ matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, 0], [0, 0, 1]]
14
+
15
+ when 'translateY'
16
+ y = arguments.first
17
+ matrix *= Matrix[[1, 0, 0], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]
18
+
19
+ when 'rotate'
20
+ angle, x, y = arguments.collect { |a| a.to_f }
21
+ angle = angle * Math::PI / 180.0
22
+
23
+ case arguments.length
24
+ when 1
25
+ matrix *= Matrix[[Math.cos(angle), Math.sin(angle), 0], [-Math.sin(angle), Math.cos(angle), 0], [0, 0, 1]]
26
+ when 3
27
+ matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]
28
+ matrix *= Matrix[[Math.cos(angle), Math.sin(angle), 0], [-Math.sin(angle), Math.cos(angle), 0], [0, 0, 1]]
29
+ matrix *= Matrix[[1, 0, -x_pixels(x.to_f)], [0, 1, y_pixels(y.to_f)], [0, 0, 1]]
30
+ else
31
+ warnings << "transform 'rotate' must have either one or three arguments"
32
+ end
33
+
34
+ when 'scale'
35
+ x_scale = arguments[0].to_f
36
+ y_scale = (arguments[1] || x_scale).to_f
37
+ matrix *= Matrix[[x_scale, 0, 0], [0, y_scale, 0], [0, 0, 1]]
38
+
39
+ when 'skewX'
40
+ angle = arguments[0].to_f * Math::PI / 180.0
41
+ matrix *= Matrix[[1, -Math.tan(angle), 0], [0, 1, 0], [0, 0, 1]]
42
+
43
+ when 'skewY'
44
+ angle = arguments[0].to_f * Math::PI / 180.0
45
+ matrix *= Matrix[[1, 0, 0], [-Math.tan(angle), 1, 0], [0, 0, 1]]
46
+
47
+ when 'matrix'
48
+ if arguments.length != 6
49
+ warnings << "transform 'matrix' must have six arguments"
50
+ else
51
+ a, b, c, d, e, f = arguments.collect { |argument| argument.to_f }
52
+ matrix *= Matrix[[a, -c, e], [-b, d, -f], [0, 0, 1]]
53
+ end
54
+
55
+ else
56
+ warnings << "Unknown/unsupported transformation '#{name}'; ignoring"
57
+ end
58
+ end
59
+
60
+ matrix.to_a[0..1].transpose.flatten
61
+ end
62
+
63
+ private
64
+
65
+ def parse_css_method_calls(string)
66
+ string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
67
+ name, argument_string = call
68
+ arguments = argument_string.strip.split(/\s*[,\s]\s*/)
69
+ [name, arguments]
70
+ end
71
+ end
72
+ end
@@ -1,5 +1,5 @@
1
1
  module Prawn
2
2
  module SVG
3
- VERSION = '0.29.1'
3
+ VERSION = '0.32.0'
4
4
  end
5
5
  end
data/lib/prawn-svg.rb CHANGED
@@ -10,6 +10,7 @@ require 'prawn/svg/calculators/arc_to_bezier_curve'
10
10
  require 'prawn/svg/calculators/aspect_ratio'
11
11
  require 'prawn/svg/calculators/document_sizing'
12
12
  require 'prawn/svg/calculators/pixels'
13
+ require 'prawn/svg/transform_parser'
13
14
  require 'prawn/svg/url_loader'
14
15
  require 'prawn/svg/loaders/data'
15
16
  require 'prawn/svg/loaders/file'
@@ -29,6 +30,9 @@ require 'prawn/svg/font'
29
30
  require 'prawn/svg/document'
30
31
  require 'prawn/svg/state'
31
32
 
33
+ require 'prawn/svg/extensions/additional_gradient_transforms'
34
+ Prawn::Document.prepend Prawn::SVG::Extensions::AdditionalGradientTransforms
35
+
32
36
  module Prawn
33
37
  Svg = SVG # backwards compatibility
34
38
  end
data/prawn-svg.gemspec CHANGED
@@ -17,10 +17,11 @@ Gem::Specification.new do |gem|
17
17
  gem.name = "prawn-svg"
18
18
  gem.require_paths = ["lib"]
19
19
 
20
- gem.required_ruby_version = '>= 2.1.0'
20
+ gem.required_ruby_version = '>= 2.3.0'
21
21
 
22
22
  gem.add_runtime_dependency "prawn", ">= 0.11.1", "< 3"
23
23
  gem.add_runtime_dependency "css_parser", "~> 1.6"
24
+ gem.add_runtime_dependency "rexml", "~> 3.2"
24
25
  gem.add_development_dependency "rspec", "~> 3.0"
25
- gem.add_development_dependency "rake", "~> 10.1"
26
+ gem.add_development_dependency "rake", "~> 13.0"
26
27
  end
@@ -28,34 +28,34 @@ describe "Integration test" do
28
28
  element.process
29
29
 
30
30
  expect(element.calls).to eq [
31
- ["fill_color", ["000000"], []],
32
- ["transformation_matrix", [1, 0, 0, 1, 0, 0], []],
33
- ["transformation_matrix", [1, 0, 0, 1, 0, 0], []],
34
- ["save", [], []], ["restore", [], []],
35
- ["save", [], []],
36
- ["fill_color", ["0000ff"], []],
37
- ["fill", [], [
38
- ["rectangle", [[0.0, 200.0], 10.0, 10.0], []]
31
+ ["fill_color", ["000000"], {}, []],
32
+ ["transformation_matrix", [1, 0, 0, 1, 0, 0], {}, []],
33
+ ["transformation_matrix", [1, 0, 0, 1, 0, 0], {}, []],
34
+ ["save", [], {}, []], ["restore", [], {}, []],
35
+ ["save", [], {}, []],
36
+ ["fill_color", ["0000ff"], {}, []],
37
+ ["fill", [], {}, [
38
+ ["rectangle", [[0.0, 200.0], 10.0, 10.0], {}, []]
39
39
  ]],
40
- ["restore", [], []],
41
- ["save", [], []],
42
- ["fill_color", ["008000"], []],
43
- ["fill", [], [
44
- ["rectangle", [[10.0, 200.0], 10.0, 10.0], []]
40
+ ["restore", [], {}, []],
41
+ ["save", [], {}, []],
42
+ ["fill_color", ["008000"], {}, []],
43
+ ["fill", [], {}, [
44
+ ["rectangle", [[10.0, 200.0], 10.0, 10.0], {}, []]
45
45
  ]],
46
- ["restore", [], []],
47
- ["save", [], []],
48
- ["fill_color", ["ff0000"], []],
49
- ["fill", [], [
50
- ["rectangle", [[20.0, 200.0], 10.0, 10.0], []]
46
+ ["restore", [], {}, []],
47
+ ["save", [], {}, []],
48
+ ["fill_color", ["ff0000"], {}, []],
49
+ ["fill", [], {}, [
50
+ ["rectangle", [[20.0, 200.0], 10.0, 10.0], {}, []]
51
51
  ]],
52
- ["restore", [], []],
53
- ["save", [], []],
54
- ["fill_color", ["ffff00"], []],
55
- ["fill", [], [
56
- ["rectangle", [[30.0, 200.0], 10.0, 10.0], []]
52
+ ["restore", [], {}, []],
53
+ ["save", [], {}, []],
54
+ ["fill_color", ["ffff00"], {}, []],
55
+ ["fill", [], {}, [
56
+ ["rectangle", [[30.0, 200.0], 10.0, 10.0], {}, []]
57
57
  ]],
58
- ["restore", [], []]
58
+ ["restore", [], {}, []]
59
59
  ]
60
60
  end
61
61
  end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+
3
+ describe Prawn::SVG::Attributes::Opacity do
4
+ class OpacityTestElement
5
+ include Prawn::SVG::Attributes::Opacity
6
+
7
+ attr_accessor :properties, :state
8
+
9
+ def initialize
10
+ @properties = ::Prawn::SVG::Properties.new
11
+ @state = ::Prawn::SVG::State.new
12
+ end
13
+
14
+ def clamp(value, min_value, max_value)
15
+ [[value, min_value].max, max_value].min
16
+ end
17
+ end
18
+
19
+ let(:element) { OpacityTestElement.new }
20
+
21
+ describe "#parse_opacity_attributes_and_call" do
22
+ subject { element.parse_opacity_attributes_and_call }
23
+
24
+ context "with no opacity specified" do
25
+ it "does nothing" do
26
+ expect(element).not_to receive(:add_call_and_enter)
27
+ subject
28
+ end
29
+ end
30
+
31
+ context "with opacity" do
32
+ it "sets fill and stroke opacity" do
33
+ element.properties.opacity = '0.4'
34
+
35
+ expect(element).to receive(:add_call_and_enter).with('transparent', 0.4, 0.4)
36
+ subject
37
+
38
+ expect(element.state.fill_opacity).to eq 0.4
39
+ expect(element.state.stroke_opacity).to eq 0.4
40
+ end
41
+ end
42
+
43
+ context "with just fill opacity" do
44
+ it "sets fill opacity and sets stroke opacity to 1" do
45
+ element.properties.fill_opacity = '0.4'
46
+
47
+ expect(element).to receive(:add_call_and_enter).with('transparent', 0.4, 1)
48
+ subject
49
+
50
+ expect(element.state.fill_opacity).to eq 0.4
51
+ expect(element.state.stroke_opacity).to eq 1
52
+ end
53
+ end
54
+
55
+ context "with an existing fill/stroke opacity" do
56
+ it "multiplies the new opacity by the old" do
57
+ element.state.fill_opacity = 0.5
58
+ element.state.stroke_opacity = 0.8
59
+
60
+ element.properties.fill_opacity = '0.4'
61
+ element.properties.stroke_opacity = '0.5'
62
+
63
+ expect(element).to receive(:add_call_and_enter).with('transparent', 0.2, 0.4)
64
+ subject
65
+
66
+ expect(element.state.fill_opacity).to eq 0.2
67
+ expect(element.state.stroke_opacity).to eq 0.4
68
+ end
69
+ end
70
+
71
+ context "with stroke, fill, and opacity all specified" do
72
+ it "choses the lower of them" do
73
+ element.properties.fill_opacity = '0.4'
74
+ element.properties.stroke_opacity = '0.6'
75
+ element.properties.opacity = '0.5'
76
+
77
+ expect(element).to receive(:add_call_and_enter).with('transparent', 0.4, 0.5)
78
+ subject
79
+
80
+ expect(element.state.fill_opacity).to eq 0.4
81
+ expect(element.state.stroke_opacity).to eq 0.5
82
+ end
83
+ end
84
+ end
85
+ end
@@ -14,43 +14,38 @@ describe Prawn::SVG::Attributes::Transform do
14
14
 
15
15
  let(:element) { TransformTestElement.new }
16
16
 
17
- describe "#parse_transform_attribute_and_call" do
18
- subject { element.send :parse_transform_attribute_and_call }
19
-
20
- describe "translate" do
21
- it "handles a missing y argument" do
22
- expect(element).to receive(:add_call_and_enter).with('translate', -5.5, 0)
23
- expect(element).to receive(:x_pixels).with(-5.5).and_return(-5.5)
24
- expect(element).to receive(:y_pixels).with(0.0).and_return(0.0)
25
-
26
- element.attributes['transform'] = 'translate(-5.5)'
27
- subject
28
- end
17
+ subject { element.send :parse_transform_attribute_and_call }
18
+
19
+ context "when a non-identity matrix is requested" do
20
+ let(:transform) { 'translate(-5.5)' }
21
+
22
+ it "passes the transform and executes the returned matrix" do
23
+ expect(element).to receive(:parse_transform_attribute).with(transform).and_return([1, 2, 3, 4, 5, 6])
24
+ expect(element).to receive(:add_call_and_enter).with('transformation_matrix', 1, 2, 3, 4, 5, 6)
25
+
26
+ element.attributes['transform'] = transform
27
+ subject
28
+ end
29
+ end
30
+
31
+ context "when an identity matrix is requested" do
32
+ let(:transform) { 'translate(0)' }
33
+
34
+ it "does not execute any commands" do
35
+ expect(element).to receive(:parse_transform_attribute).with(transform).and_return([1, 0, 0, 1, 0, 0])
36
+ expect(element).not_to receive(:add_call_and_enter)
37
+
38
+ element.attributes['transform'] = transform
39
+ subject
29
40
  end
41
+ end
42
+
43
+ context "when transform is blank" do
44
+ it "does nothing" do
45
+ expect(element).not_to receive(:parse_transform_attribute)
46
+ expect(element).not_to receive(:add_call_and_enter)
30
47
 
31
- describe "rotate" do
32
- it "handles a single angle argument" do
33
- expect(element).to receive(:add_call_and_enter).with('rotate', -5.5, :origin => [0, 0])
34
- expect(element).to receive(:y).with('0').and_return(0)
35
-
36
- element.attributes['transform'] = 'rotate(5.5)'
37
- subject
38
- end
39
-
40
- it "handles three arguments" do
41
- expect(element).to receive(:add_call_and_enter).with('rotate', -5.5, :origin => [1.0, 2.0])
42
- expect(element).to receive(:x).with(1.0).and_return(1.0)
43
- expect(element).to receive(:y).with(2.0).and_return(2.0)
44
-
45
- element.attributes['transform'] = 'rotate(5.5 1 2)'
46
- subject
47
- end
48
-
49
- it "does nothing and warns if two arguments" do
50
- expect(element).to receive(:warnings).and_return([])
51
- element.attributes['transform'] = 'rotate(5.5 1)'
52
- subject
53
- end
48
+ subject
54
49
  end
55
50
  end
56
51
  end
@@ -68,25 +68,7 @@ RSpec.describe Prawn::SVG::CSS::Stylesheets do
68
68
  [4, [["fill", "#ff0000", false], ["fill", "#330000", false], ["fill", "#440000", false], ["fill", "#00ff00", false]]],
69
69
  ]
70
70
 
71
- #
72
- # Under ruby < 2.6, a bug in REXML causes the /following-sibling selector to
73
- # only pick the first matching sibling. This means the + CSS combinator behaves
74
- # incorrectly in the following example:
75
- #
76
- # <a>
77
- # <b a="1" />
78
- # <b a="2" />
79
- # <b a="3" />
80
- # </a>
81
- #
82
- # The css selector `a b + b` will only pick the second <b>, whereas it should
83
- # pick both the second and third <b> elements.
84
- #
85
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.6.0')
86
- expected << [5, [["fill", "#ff0000", false], ["fill", "#330000", false], ["fill", "#330000", false], ["fill", "#00ff00", false]]]
87
- else
88
- expected << [5, [["fill", "#ff0000", false], ["fill", "#330000", false], ["fill", "#330000", false], ["fill", "#440000", false], ["fill", "#00ff00", false]]]
89
- end
71
+ expected << [5, [["fill", "#ff0000", false], ["fill", "#330000", false], ["fill", "#330000", false], ["fill", "#440000", false], ["fill", "#00ff00", false]]]
90
72
 
91
73
  expected.concat [
92
74
  [6, [["fill", "#ff0000", false], ["fill", "#441234", false], ["fill", "#0000ff", false]]],
@@ -25,19 +25,19 @@ describe Prawn::SVG::Elements::Base do
25
25
  describe "applying calls from the standard attributes" do
26
26
  let(:svg) do
27
27
  <<-SVG
28
- <something transform="rotate(90)" fill-opacity="0.5" fill="red" stroke="blue" stroke-width="5"/>
28
+ <something transform="scale(2)" fill-opacity="0.5" fill="red" stroke="blue" stroke-width="5"/>
29
29
  SVG
30
30
  end
31
31
 
32
32
  it "appends the relevant calls" do
33
33
  element.process
34
34
  expect(element.base_calls).to eq [
35
- ["rotate", [-90.0, {origin: [0, 600.0]}], [
36
- ["transparent", [0.5, 1], [
37
- ["fill_color", ["ff0000"], []],
38
- ["stroke_color", ["0000ff"], []],
39
- ["line_width", [5.0], []],
40
- ["fill_and_stroke", [], []]
35
+ ["transformation_matrix", [2, 0, 0, 2, 0, 0], {}, [
36
+ ["transparent", [0.5, 1], {}, [
37
+ ["fill_color", ["ff0000"], {}, []],
38
+ ["stroke_color", ["0000ff"], {}, []],
39
+ ["line_width", [5.0], {}, []],
40
+ ["fill_and_stroke", [], {}, []]
41
41
  ]]
42
42
  ]]
43
43
  ]
@@ -50,37 +50,37 @@ describe Prawn::SVG::Elements::Base do
50
50
 
51
51
  context "with neither fill nor stroke" do
52
52
  let(:svg) { '<rect style="fill: none;"></rect>' }
53
- it { is_expected.to eq ['end_path', [], []] }
53
+ it { is_expected.to eq ['end_path', [], {}, []] }
54
54
  end
55
55
 
56
56
  context "with a fill only" do
57
57
  let(:svg) { '<rect style="fill: black;"></rect>' }
58
- it { is_expected.to eq ['fill', [], []] }
58
+ it { is_expected.to eq ['fill', [], {}, []] }
59
59
  end
60
60
 
61
61
  context "with a stroke only" do
62
62
  let(:svg) { '<rect style="fill: none; stroke: black;"></rect>' }
63
- it { is_expected.to eq ['stroke', [], []] }
63
+ it { is_expected.to eq ['stroke', [], {}, []] }
64
64
  end
65
65
 
66
66
  context "with fill and stroke" do
67
67
  let(:svg) { '<rect style="fill: black; stroke: black;"></rect>' }
68
- it { is_expected.to eq ['fill_and_stroke', [], []] }
68
+ it { is_expected.to eq ['fill_and_stroke', [], {}, []] }
69
69
  end
70
70
 
71
71
  context "with fill with evenodd fill rule" do
72
72
  let(:svg) { '<rect style="fill: black; fill-rule: evenodd;"></rect>' }
73
- it { is_expected.to eq ['fill', [{fill_rule: :even_odd}], []] }
73
+ it { is_expected.to eq ['fill', [], {fill_rule: :even_odd}, []] }
74
74
  end
75
75
  end
76
76
 
77
77
  it "appends calls to the parent element" do
78
78
  expect(element).to receive(:apply) do
79
- element.send :add_call, "test", "argument"
79
+ element.send :add_call, "test", "argument", kw: 'argument'
80
80
  end
81
81
 
82
82
  element.process
83
- expect(element.parent_calls).to eq [["fill", [], [["test", ["argument"], []]]]]
83
+ expect(element.parent_calls).to eq [["fill", [], {}, [["test", ["argument"], {kw: 'argument'}, []]]]]
84
84
  end
85
85
 
86
86
  it "quietly absorbs a SkipElementQuietly exception" do
@@ -149,8 +149,8 @@ describe Prawn::SVG::Elements::Base do
149
149
  it "uses the parent's color element if 'currentColor' fill attribute provided" do
150
150
  element.process
151
151
 
152
- expect(flattened_calls).to include ['fill_color', ['ff0000']]
153
- expect(flattened_calls).not_to include ['fill_color', ['00ff00']]
152
+ expect(flattened_calls).to include ['fill_color', ['ff0000'], {}]
153
+ expect(flattened_calls).not_to include ['fill_color', ['00ff00'], {}]
154
154
  end
155
155
  end
156
156