prawn-svg 0.28.0 → 0.32.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +18 -0
  3. data/LICENSE +1 -1
  4. data/README.md +14 -9
  5. data/lib/prawn-svg.rb +4 -0
  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/stylesheets.rb +40 -19
  10. data/lib/prawn/svg/elements.rb +2 -0
  11. data/lib/prawn/svg/elements/base.rb +11 -5
  12. data/lib/prawn/svg/elements/call_duplicator.rb +1 -1
  13. data/lib/prawn/svg/elements/gradient.rb +83 -25
  14. data/lib/prawn/svg/elements/image.rb +2 -2
  15. data/lib/prawn/svg/elements/path.rb +42 -29
  16. data/lib/prawn/svg/elements/root.rb +4 -1
  17. data/lib/prawn/svg/elements/text_component.rb +21 -5
  18. data/lib/prawn/svg/elements/use.rb +23 -7
  19. data/lib/prawn/svg/extensions/additional_gradient_transforms.rb +23 -0
  20. data/lib/prawn/svg/interface.rb +38 -8
  21. data/lib/prawn/svg/loaders/data.rb +1 -1
  22. data/lib/prawn/svg/loaders/file.rb +4 -2
  23. data/lib/prawn/svg/properties.rb +1 -0
  24. data/lib/prawn/svg/transform_parser.rb +72 -0
  25. data/lib/prawn/svg/version.rb +1 -1
  26. data/prawn-svg.gemspec +3 -3
  27. data/spec/integration_spec.rb +24 -24
  28. data/spec/prawn/svg/attributes/opacity_spec.rb +85 -0
  29. data/spec/prawn/svg/attributes/transform_spec.rb +30 -35
  30. data/spec/prawn/svg/calculators/document_sizing_spec.rb +4 -4
  31. data/spec/prawn/svg/css/stylesheets_spec.rb +17 -6
  32. data/spec/prawn/svg/elements/base_spec.rb +16 -16
  33. data/spec/prawn/svg/elements/gradient_spec.rb +79 -4
  34. data/spec/prawn/svg/elements/line_spec.rb +12 -12
  35. data/spec/prawn/svg/elements/marker_spec.rb +27 -27
  36. data/spec/prawn/svg/elements/path_spec.rb +29 -17
  37. data/spec/prawn/svg/elements/polygon_spec.rb +9 -9
  38. data/spec/prawn/svg/elements/polyline_spec.rb +7 -7
  39. data/spec/prawn/svg/elements/text_spec.rb +65 -50
  40. data/spec/prawn/svg/loaders/data_spec.rb +8 -0
  41. data/spec/prawn/svg/pathable_spec.rb +4 -4
  42. data/spec/prawn/svg/transform_parser_spec.rb +94 -0
  43. data/spec/sample_svg/double_opacity.svg +6 -0
  44. data/spec/sample_svg/gradient_transform.svg +19 -0
  45. data/spec/sample_svg/links.svg +18 -0
  46. data/spec/sample_svg/radgrad01-bounding.svg +26 -0
  47. data/spec/sample_svg/radgrad01.svg +26 -0
  48. data/spec/sample_svg/svg_fill.svg +5 -0
  49. data/spec/sample_svg/text-decoration.svg +4 -0
  50. data/spec/sample_svg/transform.svg +20 -0
  51. data/spec/sample_svg/use_disordered.svg +17 -0
  52. data/spec/spec_helper.rb +2 -2
  53. metadata +48 -11
  54. data/.travis.yml +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5f5f1b932d2ff278ac37b5957a18b57d51013345
4
- data.tar.gz: 7502fc93ebe947b1955d478292bf71c7b5dd40c0
2
+ SHA256:
3
+ metadata.gz: d054ded3422dc4cd1cb870dc09549c0a82c1e2813e314565c7a3aec70d95d188
4
+ data.tar.gz: 123a00b97f78f284a8e11fad37fed42cbec06d678675ec15ff7dafbcfe6505d5
5
5
  SHA512:
6
- metadata.gz: 770335f6cd4fbff5a30d6d6ff965ff2148e66543342de66a320583f4f24ec040d341054749c619c7a9ef1491e1e0155158ba63ea407b440406791dec60fc57f2
7
- data.tar.gz: 8e1d9b9a00984309065c1ffc93b7695aafb97d4fbc91c3b6b76d7cc9ee4fe106365b23672f7a87fe0edc97e2612a7d7d3adfe557d5aaa1af4a0c90fb15652717
6
+ metadata.gz: 4ece284ced16e7b6f4d7562c17a97780375f4e255eb4199d489cfb1fa473dc1f65e6b9fa48894079f0756b42a749cc58f34c00507e19ccdfddab55723c82311d
7
+ data.tar.gz: 8826f6f457372cc7df38869e6448974f5731fdee1040bca44b8446b71c57e0713340481a3400705c8fd11ec5d13959dcd99f24a0fe8820c89a37ca725545656c
@@ -0,0 +1,18 @@
1
+ name: test
2
+ on: [push, pull_request]
3
+ jobs:
4
+ rake:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ fail-fast: false
8
+ matrix:
9
+ ruby: [2.3, 2.4, 2.5, 2.6, 2.7, '3.0']
10
+ steps:
11
+ - uses: actions/checkout@v2
12
+ - name: Set up Ruby
13
+ uses: ruby/setup-ruby@v1
14
+ with:
15
+ bundler-cache: true
16
+ ruby-version: ${{ matrix.ruby }}
17
+ - name: Run tests
18
+ run: bundle exec rake
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License
2
2
 
3
- Copyright 2010-2013 Roger Nesbitt
3
+ Copyright 2010-2019 Roger Nesbitt
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # prawn-svg
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/prawn-svg.svg)](https://badge.fury.io/rb/prawn-svg)
4
- [![Build Status](https://travis-ci.org/mogest/prawn-svg.svg?branch=master)](https://travis-ci.org/mogest/prawn-svg)
4
+ ![Build Status](https://github.com/mogest/prawn-svg/actions/workflows/test.yml/badge.svg?branch=master)
5
5
 
6
6
  An SVG renderer for the Prawn PDF library.
7
7
 
@@ -10,7 +10,7 @@ This will take an SVG document as input and render it into your PDF. Find out m
10
10
  http://github.com/prawnpdf/prawn
11
11
 
12
12
  prawn-svg is compatible with all versions of Prawn from 0.11.1 onwards, including the 1.x and 2.x series.
13
- The minimum Ruby version required is 2.1.0.
13
+ The minimum Ruby version required is 2.3.0.
14
14
 
15
15
  ## Using prawn-svg
16
16
 
@@ -60,8 +60,8 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
60
60
  - <tt>&lt;path&gt;</tt> supports all commands defined in SVG 1.1, although the
61
61
  implementation of elliptical arc is a bit rough at the moment.
62
62
 
63
- - `<text>`, `<tspan>` and `<tref>` with attributes `x`, `y`, `dx`, `dy`, `rotate`, and with extra properties
64
- `text-anchor`, `font-size`, `font-family`, `font-weight`, `font-style`, `letter-spacing`
63
+ - `<text>`, `<tspan>` and `<tref>` with attributes `x`, `y`, `dx`, `dy`, `rotate`, 'textLength', 'lengthAdjust', and with extra properties
64
+ `text-anchor`, `text-decoration` (underline only), `font-size`, `font-family`, `font-weight`, `font-style`, `letter-spacing`
65
65
 
66
66
  - <tt>&lt;svg&gt;</tt>, <tt>&lt;g&gt;</tt> and <tt>&lt;symbol&gt;</tt>
67
67
 
@@ -76,7 +76,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
76
76
 
77
77
  - `<marker>`
78
78
 
79
- - `<linearGradient>` is implemented with Prawn 2.2.0+ (gradientTransform, spreadMethod and stop-opacity are unimplemented.)
79
+ - `<linearGradient>` and `<radialGradient>` are implemented on Prawn 2.2.0+ with attributes `gradientUnits` and `gradientTransform` (spreadMethod and stop-opacity are unimplemented.)
80
80
 
81
81
  - `<switch>` and `<foreignObject>`, although prawn-svg cannot handle any data that is not SVG so `<foreignObject>`
82
82
  tags are always ignored.
@@ -91,7 +91,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
91
91
 
92
92
  - the <tt>preserveAspectRatio</tt> attribute on <tt>&lt;svg&gt;</tt>, <tt>&lt;image&gt;</tt> and `<marker>` elements
93
93
 
94
- - transform methods: <tt>translate()</tt>, <tt>rotate()</tt>, <tt>scale()</tt>, <tt>matrix()</tt>
94
+ - transform methods: `translate`, `translateX`, `translateY`, `rotate`, `scale`, `skewX`, `skewY`, `matrix`
95
95
 
96
96
  - colors: HTML standard names, <tt>#xxx</tt>, <tt>#xxxxxx</tt>, <tt>rgb(1, 2, 3)</tt>, <tt>rgb(1%, 2%, 3%)</tt>
97
97
 
@@ -107,12 +107,16 @@ In CSS selectors you can use element names, IDs, classes, attributes (existence,
107
107
  and all combinators (` `, `>`, `+`, `~`).
108
108
  The pseudo-classes `:first-child`, `:last-child` and `:nth-child(n)` (where n is a number) also work.
109
109
 
110
+ Warning: Ruby versions less than 2.6.0 have a bug in the REXML XPath implementation which means under some
111
+ conditions the `+` combinator will not pick up all matching elements. See `stylesheets_spec.rb` for an
112
+ explanation if you're stuck on an old version of Ruby.
113
+
110
114
  Pseudo-elements and the other pseudo-classes are not supported. Specificity ordering is
111
115
  implemented, but `!important` is not.
112
116
 
113
117
  ## Not supported
114
118
 
115
- prawn-svg does not support radial gradients, patterns or filters.
119
+ prawn-svg does not support hyperlinks, patterns or filters.
116
120
 
117
121
  It does not support text in the clip area, but you can clip shapes and text by any shape.
118
122
 
@@ -131,5 +135,6 @@ Mac OS X and Debian Linux users. You can add to the font path:
131
135
 
132
136
  In your Gemfile, put `gem 'prawn-svg'` before `gem 'prawn-rails'` so that prawn-rails can see the prawn-svg extension.
133
137
 
134
- --
135
- Copyright Roger Nesbitt <roger@seriousorange.com>. MIT licence.
138
+ ## Licence
139
+
140
+ MIT licence. Copyright Roger Nesbitt.
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
@@ -1,13 +1,13 @@
1
1
  module Prawn::SVG::Attributes::Opacity
2
2
  def parse_opacity_attributes_and_call
3
3
  # We can't do nested opacities quite like the SVG requires, but this is close enough.
4
- fill_opacity = stroke_opacity = clamp(properties.opacity.to_f, 0, 1) if properties.opacity
4
+ opacity = clamp(properties.opacity.to_f, 0, 1) if properties.opacity
5
5
  fill_opacity = clamp(properties.fill_opacity.to_f, 0, 1) if properties.fill_opacity
6
6
  stroke_opacity = clamp(properties.stroke_opacity.to_f, 0, 1) if properties.stroke_opacity
7
7
 
8
- if fill_opacity || stroke_opacity
9
- state.fill_opacity *= fill_opacity || 1
10
- state.stroke_opacity *= stroke_opacity || 1
8
+ if opacity || fill_opacity || stroke_opacity
9
+ state.fill_opacity *= [opacity || 1, fill_opacity || 1].min
10
+ state.stroke_opacity *= [opacity || 1, stroke_opacity || 1].min
11
11
 
12
12
  add_call_and_enter 'transparent', state.fill_opacity, state.stroke_opacity
13
13
  end
@@ -2,49 +2,7 @@ module Prawn::SVG::Attributes::Transform
2
2
  def parse_transform_attribute_and_call
3
3
  return unless transform = attributes['transform']
4
4
 
5
- parse_css_method_calls(transform).each do |name, arguments|
6
- case name
7
- when 'translate'
8
- x, y = arguments
9
- add_call_and_enter name, x_pixels(x.to_f), -y_pixels(y.to_f)
10
-
11
- when 'rotate'
12
- r, x, y = arguments.collect {|a| a.to_f}
13
- case arguments.length
14
- when 1
15
- add_call_and_enter name, -r, :origin => [0, y('0')]
16
- when 3
17
- add_call_and_enter name, -r, :origin => [x(x), y(y)]
18
- else
19
- warnings << "transform 'rotate' must have either one or three arguments"
20
- end
21
-
22
- when 'scale'
23
- x_scale = arguments[0].to_f
24
- y_scale = (arguments[1] || x_scale).to_f
25
- add_call_and_enter "transformation_matrix", x_scale, 0, 0, y_scale, 0, 0
26
-
27
- when 'matrix'
28
- if arguments.length != 6
29
- warnings << "transform 'matrix' must have six arguments"
30
- else
31
- a, b, c, d, e, f = arguments.collect {|argument| argument.to_f}
32
- add_call_and_enter "transformation_matrix", a, -b, -c, d, x_pixels(e), -y_pixels(f)
33
- end
34
-
35
- else
36
- warnings << "Unknown transformation '#{name}'; ignoring"
37
- end
38
- end
39
- end
40
-
41
- private
42
-
43
- def parse_css_method_calls(string)
44
- string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
45
- name, argument_string = call
46
- arguments = argument_string.strip.split(/\s*[,\s]\s*/)
47
- [name, arguments]
48
- end
5
+ matrix = parse_transform_attribute(transform)
6
+ add_call_and_enter "transformation_matrix", *matrix unless matrix == [1, 0, 0, 1, 0, 0]
49
7
  end
50
8
  end
@@ -36,10 +36,10 @@ module Prawn::SVG::Calculators
36
36
  @x_offset, @y_offset, @viewport_width, @viewport_height = values.map {|value| value.to_f}
37
37
 
38
38
  if @viewport_width > 0 && @viewport_height > 0
39
- # If neither the width nor height was specified, use the entire space available
39
+ # If neither the width nor height was specified, use the entire width and the viewbox ratio
40
+ # to determine the height.
40
41
  if @output_width.nil? && @output_height.nil?
41
42
  @output_width = container_width
42
- @output_height = container_height
43
43
  end
44
44
 
45
45
  # If one of the output dimensions is missing, calculate it from the other one
@@ -32,13 +32,14 @@ module Prawn::SVG::CSS
32
32
  rule_set.each_declaration { |*data| declarations << data }
33
33
 
34
34
  rule_set.selectors.each do |selector_text|
35
- selector = Prawn::SVG::CSS::SelectorParser.parse(selector_text)
36
- xpath = css_selector_to_xpath(selector)
37
- specificity = calculate_specificity(selector)
38
- specificity << order
39
- order += 1
35
+ if selector = Prawn::SVG::CSS::SelectorParser.parse(selector_text)
36
+ xpath = css_selector_to_xpath(selector)
37
+ specificity = calculate_specificity(selector)
38
+ specificity << order
39
+ order += 1
40
40
 
41
- xpath_styles << [xpath, declarations, specificity]
41
+ xpath_styles << [xpath, declarations, specificity]
42
+ end
42
43
  end
43
44
  end
44
45
 
@@ -64,20 +65,48 @@ module Prawn::SVG::CSS
64
65
  def css_selector_to_xpath(selector)
65
66
  selector.map do |element|
66
67
  pseudo_classes = Set.new(element[:pseudo_class])
68
+ require_function_name = false
67
69
 
68
70
  result = case element[:combinator]
69
71
  when :child
70
- '/'
72
+ "/"
71
73
  when :adjacent
72
74
  pseudo_classes << 'first-child'
73
- '/following-sibling::'
75
+ "/following-sibling::"
74
76
  when :siblings
75
- '/following-sibling::'
77
+ "/following-sibling::"
76
78
  else
77
- '//'
79
+ "//"
78
80
  end
79
81
 
80
- result << element[:name] if element[:name]
82
+ positions = []
83
+ pseudo_classes.each do |pc|
84
+ case pc
85
+ when "first-child" then positions << '1'
86
+ when "last-child" then positions << 'last()'
87
+ when /^nth-child\((\d+)\)$/ then positions << $1
88
+ end
89
+ end
90
+
91
+ if !positions.empty?
92
+ result << "*" unless require_function_name
93
+ require_function_name = true
94
+
95
+ logic = if positions.length == 1
96
+ positions.first
97
+ else
98
+ positions.map { |position| "position()=#{position}" }.join(" and ")
99
+ end
100
+
101
+ result << "[#{logic}]"
102
+ end
103
+
104
+ if require_function_name
105
+ result << "[name()=#{xpath_quote element[:name]}]" if element[:name]
106
+ else
107
+ result << (element[:name] || '*')
108
+ end
109
+
81
110
  result << ((element[:class] || []).map { |name| "[contains(concat(' ',@class,' '), ' #{name} ')]" }.join)
82
111
  result << ((element[:id] || []).map { |name| "[@id='#{name}']" }.join)
83
112
 
@@ -100,14 +129,6 @@ module Prawn::SVG::CSS
100
129
  end
101
130
  end
102
131
 
103
- pseudo_classes.each do |pc|
104
- case pc
105
- when "first-child" then result << "[1]"
106
- when "last-child" then result << "[last()]"
107
- when /^nth-child\((\d+)\)$/ then result << "[#{$1}]"
108
- end
109
- end
110
-
111
132
  result
112
133
  end.join
113
134
  end
@@ -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,
@@ -11,6 +11,8 @@ class Prawn::SVG::Elements::Base
11
11
  include Prawn::SVG::Attributes::Stroke
12
12
  include Prawn::SVG::Attributes::Space
13
13
 
14
+ include Prawn::SVG::TransformParser
15
+
14
16
  PAINT_TYPES = %w(fill stroke)
15
17
  COMMA_WSP_REGEXP = Prawn::SVG::Elements::COMMA_WSP_REGEXP
16
18
  SVG_NAMESPACE = "http://www.w3.org/2000/svg"
@@ -89,12 +91,12 @@ class Prawn::SVG::Elements::Base
89
91
  parse_xml_space_attribute
90
92
  end
91
93
 
92
- def add_call(name, *arguments)
93
- @calls << [name.to_s, arguments, []]
94
+ def add_call(name, *arguments, **kwarguments)
95
+ @calls << [name.to_s, arguments, kwarguments, []]
94
96
  end
95
97
 
96
- def add_call_and_enter(name, *arguments)
97
- @calls << [name.to_s, arguments, []]
98
+ def add_call_and_enter(name, *arguments, **kwarguments)
99
+ @calls << [name.to_s, arguments, kwarguments, []]
98
100
  @calls = @calls.last.last
99
101
  end
100
102
 
@@ -166,7 +168,7 @@ class Prawn::SVG::Elements::Base
166
168
  command = stroke ? 'fill_and_stroke' : 'fill'
167
169
 
168
170
  if computed_properties.fill_rule == 'evenodd'
169
- add_call_and_enter(command, {fill_rule: :even_odd})
171
+ add_call_and_enter(command, fill_rule: :even_odd)
170
172
  else
171
173
  add_call_and_enter(command)
172
174
  end
@@ -265,4 +267,8 @@ class Prawn::SVG::Elements::Base
265
267
  element = document.elements_by_id[matches[1]] if matches
266
268
  element if element && (expected_type.nil? || element.name == expected_type)
267
269
  end
270
+
271
+ def href_attribute
272
+ attributes['xlink:href'] || attributes['href']
273
+ end
268
274
  end
@@ -12,7 +12,7 @@ module Prawn::SVG::Elements::CallDuplicator
12
12
  end
13
13
 
14
14
  def duplicate_call(call)
15
- [call[0], duplicate_array(call[1]), duplicate_calls(call[2])]
15
+ [call[0], duplicate_array(call[1]), duplicate_hash(call[2]), duplicate_calls(call[3])]
16
16
  end
17
17
 
18
18
  def duplicate_array(array)
@@ -1,10 +1,18 @@
1
1
  class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
2
- TAG_NAME_TO_TYPE = {"linearGradient" => :linear}
2
+ attr_reader :parent_gradient
3
+ attr_reader :x1, :y1, :x2, :y2, :cx, :cy, :fx, :fy, :radius, :units, :stops, :transform_matrix
4
+
5
+ TAG_NAME_TO_TYPE = {
6
+ "linearGradient" => :linear,
7
+ "radialGradient" => :radial
8
+ }
3
9
 
4
10
  def parse
5
11
  # A gradient tag without an ID is inaccessible and can never be used
6
12
  raise SkipElementQuietly if attributes['id'].nil?
7
13
 
14
+ @parent_gradient = document.gradients[href_attribute[1..-1]] if href_attribute && href_attribute[0] == '#'
15
+
8
16
  assert_compatible_prawn_version
9
17
  load_gradient_configuration
10
18
  load_coordinates
@@ -16,26 +24,56 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
16
24
  end
17
25
 
18
26
  def gradient_arguments(element)
19
- case @units
20
- when :bounding_box
21
- x1, y1, x2, y2 = element.bounding_box
22
- return if y2.nil?
27
+ # Passing in a transformation matrix to the apply_transformations option is supported
28
+ # by a monkey patch installed by prawn-svg. Prawn only sees this as a truthy variable.
29
+ #
30
+ # See Prawn::SVG::Extensions::AdditionalGradientTransforms for details.
31
+ base_arguments = {stops: stops, apply_transformations: transform_matrix || true}
32
+
33
+ arguments = specific_gradient_arguments(element)
34
+ arguments.merge(base_arguments) if arguments
35
+ end
23
36
 
24
- width = x2 - x1
25
- height = y1 - y2
37
+ private
26
38
 
27
- from = [x1 + width * @x1, y1 - height * @y1]
28
- to = [x1 + width * @x2, y1 - height * @y2]
39
+ def specific_gradient_arguments(element)
40
+ if units == :bounding_box
41
+ bounding_x1, bounding_y1, bounding_x2, bounding_y2 = element.bounding_box
42
+ return if bounding_y2.nil?
29
43
 
30
- when :user_space
31
- from = [@x1, @y1]
32
- to = [@x2, @y2]
44
+ width = bounding_x2 - bounding_x1
45
+ height = bounding_y1 - bounding_y2
33
46
  end
34
47
 
35
- {from: from, to: to, stops: @stops}
36
- end
48
+ case [type, units]
49
+ when [:linear, :bounding_box]
50
+ from = [bounding_x1 + width * x1, bounding_y1 - height * y1]
51
+ to = [bounding_x1 + width * x2, bounding_y1 - height * y2]
37
52
 
38
- private
53
+ {from: from, to: to}
54
+
55
+ when [:linear, :user_space]
56
+ {from: [x1, y1], to: [x2, y2]}
57
+
58
+ when [:radial, :bounding_box]
59
+ center = [bounding_x1 + width * cx, bounding_y1 - height * cy]
60
+ focus = [bounding_x1 + width * fx, bounding_y1 - height * fy]
61
+
62
+ # Note: Chrome, at least, implements radial bounding box radiuses as
63
+ # having separate X and Y components, so in bounding box mode their
64
+ # gradients come out as ovals instead of circles. PDF radial shading
65
+ # doesn't have the option to do this, and it's confusing why the
66
+ # Chrome user space gradients don't apply the same logic anyway.
67
+ hypot = Math.sqrt(width * width + height * height)
68
+ {from: focus, r1: 0, to: center, r2: radius * hypot}
69
+
70
+ when [:radial, :user_space]
71
+ {from: [fx, fy], r1: 0, to: [cx, cy], r2: radius}
72
+
73
+ else
74
+ raise "unexpected type/unit system"
75
+ end
76
+ end
39
77
 
40
78
  def type
41
79
  TAG_NAME_TO_TYPE.fetch(name)
@@ -51,10 +89,7 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
51
89
  @units = attributes["gradientUnits"] == 'userSpaceOnUse' ? :user_space : :bounding_box
52
90
 
53
91
  if transform = attributes["gradientTransform"]
54
- matrix = transform.split(COMMA_WSP_REGEXP).map(&:to_f)
55
- if matrix != [1, 0, 0, 1, 0, 0]
56
- raise SkipElementError, "prawn-svg does not yet support gradients with a non-identity gradientTransform attribute"
57
- end
92
+ @transform_matrix = parse_transform_attribute(transform)
58
93
  end
59
94
 
60
95
  if (spread_method = attributes['spreadMethod']) && spread_method != "pad"
@@ -63,18 +98,35 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
63
98
  end
64
99
 
65
100
  def load_coordinates
66
- case @units
67
- when :bounding_box
101
+ case [type, units]
102
+ when [:linear, :bounding_box]
68
103
  @x1 = parse_zero_to_one(attributes["x1"], 0)
69
104
  @y1 = parse_zero_to_one(attributes["y1"], 0)
70
105
  @x2 = parse_zero_to_one(attributes["x2"], 1)
71
106
  @y2 = parse_zero_to_one(attributes["y2"], 0)
72
107
 
73
- when :user_space
108
+ when [:linear, :user_space]
74
109
  @x1 = x(attributes["x1"])
75
110
  @y1 = y(attributes["y1"])
76
111
  @x2 = x(attributes["x2"])
77
112
  @y2 = y(attributes["y2"])
113
+
114
+ when [:radial, :bounding_box]
115
+ @cx = parse_zero_to_one(attributes["cx"], 0.5)
116
+ @cy = parse_zero_to_one(attributes["cy"], 0.5)
117
+ @fx = parse_zero_to_one(attributes["fx"], cx)
118
+ @fy = parse_zero_to_one(attributes["fy"], cy)
119
+ @radius = parse_zero_to_one(attributes["r"], 0.5)
120
+
121
+ when [:radial, :user_space]
122
+ @cx = x(attributes["cx"] || '50%')
123
+ @cy = y(attributes["cy"] || '50%')
124
+ @fx = x(attributes["fx"] || attributes["cx"])
125
+ @fy = y(attributes["fy"] || attributes["cy"])
126
+ @radius = pixels(attributes["r"] || '50%')
127
+
128
+ else
129
+ raise "unexpected type/unit system"
78
130
  end
79
131
  end
80
132
 
@@ -100,10 +152,16 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
100
152
  end
101
153
  end
102
154
 
103
- raise SkipElementError, "gradient does not have any valid stops" if @stops.empty?
155
+ if stops.empty?
156
+ if parent_gradient.nil? || parent_gradient.stops.empty?
157
+ raise SkipElementError, "gradient does not have any valid stops"
158
+ end
104
159
 
105
- @stops.unshift([0, @stops.first.last]) if @stops.first.first > 0
106
- @stops.push([1, @stops.last.last]) if @stops.last.first < 1
160
+ @stops = parent_gradient.stops
161
+ else
162
+ stops.unshift([0, stops.first.last]) if stops.first.first > 0
163
+ stops.push([1, stops.last.last]) if stops.last.first < 1
164
+ end
107
165
  end
108
166
 
109
167
  def parse_zero_to_one(string, default = 0)