prawn-svg 0.27.1 → 0.31.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 (62) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +6 -4
  3. data/LICENSE +1 -1
  4. data/README.md +23 -9
  5. data/lib/prawn-svg.rb +7 -1
  6. data/lib/prawn/svg/attributes/opacity.rb +4 -4
  7. data/lib/prawn/svg/attributes/transform.rb +2 -44
  8. data/lib/prawn/svg/calculators/document_sizing.rb +2 -2
  9. data/lib/prawn/svg/{css.rb → css/font_family_parser.rb} +3 -4
  10. data/lib/prawn/svg/css/selector_parser.rb +174 -0
  11. data/lib/prawn/svg/css/stylesheets.rb +146 -0
  12. data/lib/prawn/svg/document.rb +3 -15
  13. data/lib/prawn/svg/elements.rb +4 -2
  14. data/lib/prawn/svg/elements/base.rb +26 -23
  15. data/lib/prawn/svg/elements/clip_path.rb +12 -0
  16. data/lib/prawn/svg/elements/container.rb +1 -3
  17. data/lib/prawn/svg/elements/gradient.rb +83 -25
  18. data/lib/prawn/svg/elements/image.rb +2 -2
  19. data/lib/prawn/svg/elements/path.rb +42 -29
  20. data/lib/prawn/svg/elements/root.rb +4 -1
  21. data/lib/prawn/svg/elements/text.rb +1 -1
  22. data/lib/prawn/svg/elements/text_component.rb +63 -14
  23. data/lib/prawn/svg/elements/use.rb +23 -7
  24. data/lib/prawn/svg/extensions/additional_gradient_transforms.rb +23 -0
  25. data/lib/prawn/svg/font_registry.rb +4 -3
  26. data/lib/prawn/svg/interface.rb +26 -2
  27. data/lib/prawn/svg/loaders/data.rb +1 -1
  28. data/lib/prawn/svg/loaders/file.rb +4 -2
  29. data/lib/prawn/svg/properties.rb +2 -0
  30. data/lib/prawn/svg/state.rb +6 -3
  31. data/lib/prawn/svg/transform_parser.rb +72 -0
  32. data/lib/prawn/svg/version.rb +1 -1
  33. data/prawn-svg.gemspec +3 -4
  34. data/spec/prawn/svg/attributes/opacity_spec.rb +85 -0
  35. data/spec/prawn/svg/attributes/transform_spec.rb +30 -35
  36. data/spec/prawn/svg/calculators/document_sizing_spec.rb +4 -4
  37. data/spec/prawn/svg/{css_spec.rb → css/font_family_parser_spec.rb} +3 -3
  38. data/spec/prawn/svg/css/selector_parser_spec.rb +33 -0
  39. data/spec/prawn/svg/css/stylesheets_spec.rb +142 -0
  40. data/spec/prawn/svg/document_spec.rb +0 -33
  41. data/spec/prawn/svg/elements/base_spec.rb +58 -2
  42. data/spec/prawn/svg/elements/gradient_spec.rb +79 -4
  43. data/spec/prawn/svg/elements/path_spec.rb +29 -17
  44. data/spec/prawn/svg/elements/text_spec.rb +74 -16
  45. data/spec/prawn/svg/font_registry_spec.rb +30 -0
  46. data/spec/prawn/svg/interface_spec.rb +33 -1
  47. data/spec/prawn/svg/loaders/data_spec.rb +8 -0
  48. data/spec/prawn/svg/transform_parser_spec.rb +94 -0
  49. data/spec/sample_output/{directory → .keep} +0 -0
  50. data/spec/sample_svg/double_opacity.svg +6 -0
  51. data/spec/sample_svg/gradient_transform.svg +19 -0
  52. data/spec/sample_svg/links.svg +18 -0
  53. data/spec/sample_svg/radgrad01-bounding.svg +26 -0
  54. data/spec/sample_svg/radgrad01.svg +26 -0
  55. data/spec/sample_svg/svg_fill.svg +5 -0
  56. data/spec/sample_svg/text-decoration.svg +4 -0
  57. data/spec/sample_svg/text_stroke.svg +41 -0
  58. data/spec/sample_svg/transform.svg +20 -0
  59. data/spec/sample_svg/use_disordered.svg +17 -0
  60. data/spec/sample_svg/warning-radioactive.svg +98 -0
  61. data/spec/spec_helper.rb +2 -2
  62. metadata +137 -15
@@ -8,7 +8,10 @@ class Prawn::SVG::Elements::Root < Prawn::SVG::Elements::Base
8
8
  end
9
9
 
10
10
  def apply
11
- add_call 'fill_color', '000000'
11
+ if [nil, 'inherit', 'none', 'currentColor'].include?(properties.fill)
12
+ add_call 'fill_color', '000000'
13
+ end
14
+
12
15
  add_call 'transformation_matrix', @document.sizing.x_scale, 0, 0, @document.sizing.y_scale, 0, 0
13
16
  add_call 'transformation_matrix', 1, 0, 0, 1, -@document.sizing.x_offset, @document.sizing.y_offset
14
17
  end
@@ -7,7 +7,7 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::DepthFirstBase
7
7
  # of the element, delegating it to our root text component.
8
8
 
9
9
  def parse_step
10
- state.text = Prawn::SVG::Elements::TextComponent::PositionsList.new([], [], [], [], [], nil)
10
+ state.text = Prawn::SVG::Elements::TextComponent::TextState.new
11
11
 
12
12
  @text_root = Prawn::SVG::Elements::TextComponent.new(document, source, nil, state.dup)
13
13
  @text_root.parse_step
@@ -2,14 +2,22 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
2
2
  attr_reader :commands
3
3
 
4
4
  Printable = Struct.new(:element, :text, :leading_space?, :trailing_space?)
5
- PositionsList = Struct.new(:x, :y, :dx, :dy, :rotation, :parent)
5
+ TextState = Struct.new(:parent, :x, :y, :dx, :dy, :rotation, :spacing, :mode, :text_length, :length_adjust)
6
6
 
7
7
  def parse
8
- state.text.x = attributes['x'].split(COMMA_WSP_REGEXP).collect {|n| x(n)} if attributes['x']
9
- state.text.y = attributes['y'].split(COMMA_WSP_REGEXP).collect {|n| y(n)} if attributes['y']
10
- state.text.dx = attributes['dx'].split(COMMA_WSP_REGEXP).collect {|n| x_pixels(n)} if attributes['dx']
11
- state.text.dy = attributes['dy'].split(COMMA_WSP_REGEXP).collect {|n| y_pixels(n)} if attributes['dy']
12
- state.text.rotation = attributes['rotate'].split(COMMA_WSP_REGEXP).collect(&:to_f) if attributes['rotate']
8
+ if state.inside_clip_path
9
+ raise SkipElementError, "<text> elements are not supported in clip paths"
10
+ end
11
+
12
+ state.text.x = (attributes['x'] || "").split(COMMA_WSP_REGEXP).collect { |n| x(n) }
13
+ state.text.y = (attributes['y'] || "").split(COMMA_WSP_REGEXP).collect { |n| y(n) }
14
+ state.text.dx = (attributes['dx'] || "").split(COMMA_WSP_REGEXP).collect { |n| x_pixels(n) }
15
+ state.text.dy = (attributes['dy'] || "").split(COMMA_WSP_REGEXP).collect { |n| y_pixels(n) }
16
+ state.text.rotation = (attributes['rotate'] || "").split(COMMA_WSP_REGEXP).collect(&:to_f)
17
+ state.text.text_length = normalize_length(attributes['textLength'])
18
+ state.text.length_adjust = attributes['lengthAdjust']
19
+ state.text.spacing = calculate_character_spacing
20
+ state.text.mode = calculate_text_rendering_mode
13
21
 
14
22
  @commands = []
15
23
 
@@ -38,13 +46,18 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
38
46
  opts = {
39
47
  size: computed_properties.numerical_font_size,
40
48
  style: font && font.subfamily,
41
- text_anchor: computed_properties.text_anchor
49
+ text_anchor: computed_properties.text_anchor,
42
50
  }
43
51
 
44
- spacing = computed_properties.letter_spacing
45
- spacing = spacing == 'normal' ? 0 : pixels(spacing)
52
+ opts[:decoration] = computed_properties.text_decoration unless computed_properties.text_decoration == 'none'
46
53
 
47
- add_call_and_enter 'character_spacing', spacing
54
+ if state.text.parent
55
+ add_call_and_enter 'character_spacing', state.text.spacing unless state.text.spacing == state.text.parent.spacing
56
+ add_call_and_enter 'text_rendering_mode', state.text.mode unless state.text.mode == state.text.parent.mode
57
+ else
58
+ add_call_and_enter 'character_spacing', state.text.spacing unless state.text.spacing == 0
59
+ add_call_and_enter 'text_rendering_mode', state.text.mode unless state.text.mode == :fill
60
+ end
48
61
 
49
62
  @commands.each do |command|
50
63
  case command
@@ -59,8 +72,8 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
59
72
  end
60
73
  end
61
74
 
62
- # It's possible there was no text to render. In that case, add a 'noop' so
63
- # character_spacing doesn't blow up when it finds it doesn't have a block to execute.
75
+ # It's possible there was no text to render. In that case, add a 'noop' so character_spacing/text_rendering_mode
76
+ # don't blow up when they find they don't have a block to execute.
64
77
  add_call 'noop' if calls.empty?
65
78
  end
66
79
 
@@ -81,7 +94,7 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
81
94
 
82
95
  def append_child(child)
83
96
  new_state = state.dup
84
- new_state.text = PositionsList.new([], [], [], [], [], state.text)
97
+ new_state.text = TextState.new(state.text)
85
98
 
86
99
  element = self.class.new(document, child, calls, new_state)
87
100
  @commands << element
@@ -124,6 +137,14 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
124
137
  opts.delete(:rotate)
125
138
  end
126
139
 
140
+ if state.text.text_length
141
+ if state.text.length_adjust == 'spacingAndGlyphs'
142
+ opts[:stretch_to_width] = state.text.text_length
143
+ else
144
+ opts[:pad_to_width] = state.text.text_length
145
+ end
146
+ end
147
+
127
148
  if remaining
128
149
  add_call 'draw_text', text[0..0], opts.dup
129
150
  text = text[1..-1]
@@ -164,7 +185,7 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
164
185
  end
165
186
 
166
187
  def find_referenced_element
167
- href = attributes['xlink:href']
188
+ href = href_attribute
168
189
 
169
190
  if href && href[0..0] == '#'
170
191
  element = document.elements_by_id[href[1..-1]]
@@ -189,4 +210,32 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
189
210
  def apply_font(font)
190
211
  add_call 'font', font.name, style: font.subfamily
191
212
  end
213
+
214
+ def calculate_text_rendering_mode
215
+ fill = computed_properties.fill != 'none'
216
+ stroke = computed_properties.stroke != 'none'
217
+
218
+ if fill && stroke
219
+ :fill_stroke
220
+ elsif fill
221
+ :fill
222
+ elsif stroke
223
+ :stroke
224
+ else
225
+ :invisible
226
+ end
227
+ end
228
+
229
+ def calculate_character_spacing
230
+ spacing = computed_properties.letter_spacing
231
+ spacing == 'normal' ? 0 : pixels(spacing)
232
+ end
233
+
234
+ # overridden, we don't want to call fill/stroke as draw_text does this for us
235
+ def apply_drawing_call
236
+ end
237
+
238
+ def normalize_length(length)
239
+ x_pixels(length) if length && length.match(/\d/)
240
+ end
192
241
  end
@@ -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"
@@ -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
@@ -24,7 +24,7 @@ class Prawn::SVG::FontRegistry
24
24
  end
25
25
 
26
26
  def load(family, weight = nil, style = nil)
27
- Prawn::SVG::CSS.parse_font_family_string(family).detect do |name|
27
+ Prawn::SVG::CSS::FontFamilyParser.parse(family).detect do |name|
28
28
  name = name.gsub(/\s{2,}/, ' ').downcase
29
29
 
30
30
  font = Prawn::SVG::Font.new(name, weight, style, font_registry: self)
@@ -37,8 +37,9 @@ class Prawn::SVG::FontRegistry
37
37
  def merge_external_fonts
38
38
  if @font_case_mapping.nil?
39
39
  self.class.load_external_fonts unless self.class.external_font_families
40
- @font_families.merge!(self.class.external_font_families)
41
-
40
+ @font_families.merge!(self.class.external_font_families) do |key, v1, v2|
41
+ v1
42
+ end
42
43
  @font_case_mapping = @font_families.keys.each.with_object({}) do |key, result|
43
44
  result[key.downcase] = key
44
45
  end
@@ -119,7 +119,7 @@ module Prawn
119
119
 
120
120
  if skip
121
121
  # the call has been overridden
122
- elsif children.empty?
122
+ elsif children.empty? && call != 'transparent' # some prawn calls complain if they aren't supplied a block
123
123
  prawn.send(call, *arguments)
124
124
  else
125
125
  prawn.send(call, *arguments, &proc_creator(prawn, children))
@@ -148,6 +148,19 @@ module Prawn
148
148
 
149
149
  width = prawn.width_of(text, options.merge(kerning: true))
150
150
 
151
+ if stretch_to_width = options.delete(:stretch_to_width)
152
+ factor = stretch_to_width.to_f * 100 / width.to_f
153
+ prawn.add_content "#{factor} Tz"
154
+ width = stretch_to_width.to_f
155
+ end
156
+
157
+ if pad_to_width = options.delete(:pad_to_width)
158
+ padding_required = pad_to_width.to_f - width.to_f
159
+ padding_per_character = padding_required / text.length.to_f
160
+ prawn.add_content "#{padding_per_character} Tc"
161
+ width = pad_to_width.to_f
162
+ end
163
+
151
164
  case options.delete(:text_anchor)
152
165
  when 'middle'
153
166
  at[0] -= width / 2
@@ -159,6 +172,15 @@ module Prawn
159
172
  @cursor = [at[0] + width, at[1]]
160
173
  end
161
174
 
175
+ decoration = options.delete(:decoration)
176
+ if decoration == 'underline'
177
+ prawn.save_graphics_state do
178
+ prawn.line_width 1
179
+ prawn.line [at[0], at[1] - 1.25], [at[0] + width, at[1] - 1.25]
180
+ prawn.stroke
181
+ end
182
+ end
183
+
162
184
  when 'transformation_matrix'
163
185
  left = prawn.bounds.absolute_left
164
186
  top = prawn.bounds.absolute_top
@@ -186,7 +208,9 @@ module Prawn
186
208
  # prawn (as at 2.0.1 anyway) uses 'b' for its fill_and_stroke. 'b' is 'h' (closepath) + 'B', and we
187
209
  # never want closepath to be automatically run as it stuffs up many drawing operations, such as dashes
188
210
  # and line caps, and makes paths close that we didn't ask to be closed when fill is specified.
189
- prawn.add_content 'B'
211
+ even_odd = arguments[0].is_a?(Hash) && arguments[0][:fill_rule] == :even_odd
212
+ content = even_odd ? 'B*' : 'B'
213
+ prawn.add_content content
190
214
 
191
215
  when 'noop'
192
216
  yield
@@ -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)
@@ -85,7 +87,7 @@ module Prawn::SVG::Loaders
85
87
  end
86
88
 
87
89
  def assert_valid_file_uri!(uri)
88
- if uri.host
90
+ unless uri.host.nil? || uri.host.empty?
89
91
  raise Prawn::SVG::UrlLoader::Error, "prawn-svg does not suport file: URLs with a host. Your URL probably doesn't start with three slashes, and it should."
90
92
  end
91
93
  end
@@ -18,6 +18,7 @@ class Prawn::SVG::Properties
18
18
  "display" => Config.new("inline", false, %w(inherit inline none), true),
19
19
  "fill" => Config.new("black", true, %w(inherit none currentColor)),
20
20
  "fill-opacity" => Config.new("1", true),
21
+ "fill-rule" => Config.new("nonzero", true, %w(inherit nonzero evenodd)),
21
22
  "font-family" => Config.new("sans-serif", true),
22
23
  "font-size" => Config.new("medium", true, %w(inherit xx-small x-small small medium large x-large xx-large larger smaller)),
23
24
  "font-style" => Config.new("normal", true, %w(inherit normal italic oblique), true),
@@ -36,6 +37,7 @@ class Prawn::SVG::Properties
36
37
  "stroke-opacity" => Config.new("1", true),
37
38
  "stroke-width" => Config.new("1", true),
38
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),
39
41
  }.freeze
40
42
 
41
43
  PROPERTIES.each do |name, value|
@@ -1,10 +1,9 @@
1
1
  class Prawn::SVG::State
2
- attr_accessor :disable_drawing,
3
- :text, :preserve_space,
2
+ attr_accessor :text, :preserve_space,
4
3
  :fill_opacity, :stroke_opacity, :stroke_width,
5
4
  :computed_properties,
6
5
  :viewport_sizing,
7
- :inside_use
6
+ :inside_use, :inside_clip_path
8
7
 
9
8
  def initialize
10
9
  @stroke_width = 1
@@ -16,4 +15,8 @@ class Prawn::SVG::State
16
15
  def initialize_dup(other)
17
16
  @computed_properties = @computed_properties.dup
18
17
  end
18
+
19
+ def disable_drawing
20
+ inside_clip_path
21
+ end
19
22
  end
@@ -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.27.1'
3
+ VERSION = '0.31.0'
4
4
  end
5
5
  end