prawn-svg 0.31.0 → 0.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +35 -0
  3. data/README.md +8 -6
  4. data/lib/prawn/svg/attributes/stroke.rb +5 -0
  5. data/lib/prawn/svg/color.rb +46 -22
  6. data/lib/prawn/svg/css/selector_parser.rb +11 -11
  7. data/lib/prawn/svg/css/stylesheets.rb +4 -4
  8. data/lib/prawn/svg/document.rb +15 -2
  9. data/lib/prawn/svg/elements/base.rb +15 -16
  10. data/lib/prawn/svg/elements/call_duplicator.rb +1 -1
  11. data/lib/prawn/svg/elements/gradient.rb +2 -2
  12. data/lib/prawn/svg/elements/image.rb +6 -7
  13. data/lib/prawn/svg/elements/path.rb +0 -2
  14. data/lib/prawn/svg/elements/root.rb +1 -1
  15. data/lib/prawn/svg/elements/text_component.rb +10 -5
  16. data/lib/prawn/svg/gradients.rb +48 -0
  17. data/lib/prawn/svg/interface.rb +23 -8
  18. data/lib/prawn/svg/properties.rb +2 -0
  19. data/lib/prawn/svg/version.rb +1 -1
  20. data/lib/prawn-svg.rb +1 -0
  21. data/prawn-svg.gemspec +17 -15
  22. data/spec/integration_spec.rb +24 -24
  23. data/spec/prawn/svg/color_spec.rb +29 -16
  24. data/spec/prawn/svg/css/stylesheets_spec.rb +1 -19
  25. data/spec/prawn/svg/document_spec.rb +17 -0
  26. data/spec/prawn/svg/elements/base_spec.rb +15 -15
  27. data/spec/prawn/svg/elements/line_spec.rb +12 -12
  28. data/spec/prawn/svg/elements/marker_spec.rb +27 -27
  29. data/spec/prawn/svg/elements/polygon_spec.rb +9 -9
  30. data/spec/prawn/svg/elements/polyline_spec.rb +7 -7
  31. data/spec/prawn/svg/elements/root_spec.rb +28 -0
  32. data/spec/prawn/svg/elements/text_spec.rb +80 -53
  33. data/spec/prawn/svg/interface_spec.rb +10 -2
  34. data/spec/prawn/svg/pathable_spec.rb +4 -4
  35. data/spec/sample_svg/cmyk.svg +5 -0
  36. data/spec/sample_svg/gradients-cmyk.svg +40 -0
  37. data/spec/sample_svg/ordering.svg +12 -0
  38. data/spec/spec_helper.rb +2 -2
  39. metadata +53 -16
  40. data/.travis.yml +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ed78a64dc94709fc5e53f1d6f521f93e3bf41196c29dde616c6139743c9c889
4
- data.tar.gz: 7a0a5683370d3478e8b9717069e0f4012337aaf6250106065dfd5ad9b4458a91
3
+ metadata.gz: c9f71b82858f8ee91e1ccaf2903c2e2a0d49108b1532a81d15c2275362a0d136
4
+ data.tar.gz: f3336c6a727df24e6a5877e4d26cf030eabbb1c9af9852f95c35c5bfe5128995
5
5
  SHA512:
6
- metadata.gz: a58fd3f4508d486b3632f2e337f5e2fe97ef8c2baeeeda9b4f53f816ff18eb10b3e8879c98ea10c645becb29c8351a7addc6867c22abfeda77a7edf654607f44
7
- data.tar.gz: 3bdbbc06f25405d3f51de30e81c39406f7ccf535e507bb36b43df3939d1a4931b763cb5308bde72674ab3f2ffcb55171b958f93a3b603bf5cf17a1bbd24d44b4
6
+ metadata.gz: 94f1a140e837ab37b088cc659bc8ef63a3e89778539236441f43e7b4885b9028cce1cb83c9e9636b00ff2ac740770ec8d853c123aa42b0764fb1264405269e76
7
+ data.tar.gz: 4d02f2e14875214bb5460ace33925f9ab17302b39f46e0a37e0e5521e63b0fdd2f23f6018c8e048bf2bce7d148548aa86b5b9967dca827faddccfa23e8e09b13
@@ -0,0 +1,35 @@
1
+ name: test
2
+ on: [push, pull_request]
3
+ jobs:
4
+ rake_new:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ fail-fast: false
8
+ matrix:
9
+ ruby: [2.6, 2.7, '3.0', 3.1, 3.2]
10
+ steps:
11
+ - uses: actions/checkout@v4
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
19
+ env:
20
+ RUBYOPT: "--enable-frozen-string-literal"
21
+ rake_old:
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ fail-fast: false
25
+ matrix:
26
+ ruby: [2.5]
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - name: Set up Ruby
30
+ uses: ruby/setup-ruby@v1
31
+ with:
32
+ bundler-cache: true
33
+ ruby-version: ${{ matrix.ruby }}
34
+ - name: Run tests
35
+ run: bundle exec rake
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
 
@@ -34,6 +34,7 @@ Option | Data type | Description
34
34
  :enable_file_requests_with_root | string | If not nil, prawn-svg will serve `file:` URLs from your local disk if the file is located under the specified directory. It is very dangerous to specify the root path ("/") if you're not fully in control of your input SVG. Defaults to `nil` (off).
35
35
  :cache_images | boolean | If true, prawn-svg will cache the result of all URL requests. Defaults to false.
36
36
  :fallback_font_name | string | A font name which will override the default fallback font of Times-Roman. If this value is set to <tt>nil</tt>, prawn-svg will ignore a request for an unknown font and log a warning.
37
+ :color_mode | :rgb, :cmyk | Output color mode. Defaults to :rgb.
37
38
 
38
39
  ## Examples
39
40
 
@@ -61,7 +62,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
61
62
  implementation of elliptical arc is a bit rough at the moment.
62
63
 
63
64
  - `<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
+ `text-anchor`, `text-decoration` (underline only), `font-size`, `font-family`, `font-weight`, `font-style`, `letter-spacing`, `dominant-baseline` (middle only)
65
66
 
66
67
  - <tt>&lt;svg&gt;</tt>, <tt>&lt;g&gt;</tt> and <tt>&lt;symbol&gt;</tt>
67
68
 
@@ -69,7 +70,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
69
70
 
70
71
  - <tt>&lt;style&gt;</tt> (see CSS section below)
71
72
 
72
- - <tt>&lt;image&gt;</tt> with <tt>http:</tt>, <tt>https:</tt>, <tt>data:image/\*;base64</tt> and `file:` schemes
73
+ - `<image>` referencing a JPEG or PNG image, with `http:`, `https:`, `data:image/jpeg;base64`, `data:image/png;base64` and `file:` schemes
73
74
  (`file:` is disabled by default for security reasons, see Options section above)
74
75
 
75
76
  - <tt>&lt;clipPath&gt;</tt>
@@ -93,7 +94,8 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
93
94
 
94
95
  - transform methods: `translate`, `translateX`, `translateY`, `rotate`, `scale`, `skewX`, `skewY`, `matrix`
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
+ - colors: HTML standard names, <tt>#xxx</tt>, <tt>#xxxxxx</tt>, <tt>rgb(1, 2, 3)</tt>, <tt>rgb(1%, 2%, 3%)</tt>,
98
+ and also the non-standard `device-cmyk(1, 2, 3, 4)` for CMYK colors
97
99
 
98
100
  - measurements specified in <tt>pt</tt>, <tt>cm</tt>, <tt>dm</tt>, <tt>ft</tt>, <tt>in</tt>, <tt>m</tt>, <tt>mm</tt>, <tt>yd</tt>, <tt>pc</tt>, <tt>%</tt>
99
101
 
@@ -116,7 +118,7 @@ implemented, but `!important` is not.
116
118
 
117
119
  ## Not supported
118
120
 
119
- prawn-svg does not support hyperlinks, patterns or filters.
121
+ prawn-svg does not support hyperlinks, patterns, masks or filters.
120
122
 
121
123
  It does not support text in the clip area, but you can clip shapes and text by any shape.
122
124
 
@@ -1,5 +1,6 @@
1
1
  module Prawn::SVG::Attributes::Stroke
2
2
  CAP_STYLE_TRANSLATIONS = {"butt" => :butt, "round" => :round, "square" => :projecting_square}
3
+ JOIN_STYLE_TRANSLATIONS = {"miter" => :miter, "round" => :round, "bevel" => :bevel}
3
4
 
4
5
  def parse_stroke_attributes_and_call
5
6
  if width_string = properties.stroke_width
@@ -11,6 +12,10 @@ module Prawn::SVG::Attributes::Stroke
11
12
  if (linecap = properties.stroke_linecap) && linecap != 'inherit'
12
13
  add_call('cap_style', CAP_STYLE_TRANSLATIONS.fetch(linecap, :butt))
13
14
  end
15
+
16
+ if (linejoin = properties.stroke_linejoin) && linejoin != 'inherit'
17
+ add_call('join_style', JOIN_STYLE_TRANSLATIONS.fetch(linejoin, :miter))
18
+ end
14
19
 
15
20
  if dasharray = properties.stroke_dasharray
16
21
  case dasharray
@@ -1,17 +1,9 @@
1
1
  class Prawn::SVG::Color
2
- class Hex
3
- attr_reader :value
2
+ RGB = Struct.new(:value)
3
+ CMYK = Struct.new(:value)
4
4
 
5
- def initialize(value)
6
- @value = value
7
- end
8
-
9
- def ==(other)
10
- value == other.value
11
- end
12
- end
13
-
14
- DEFAULT_COLOR = Hex.new("000000")
5
+ RGB_DEFAULT_COLOR = RGB.new("000000")
6
+ CMYK_DEFAULT_COLOR = CMYK.new([0, 0, 0, 100])
15
7
 
16
8
  HTML_COLORS = {
17
9
  'aliceblue' => 'f0f8ff',
@@ -163,24 +155,25 @@ class Prawn::SVG::Color
163
155
  'yellowgreen' => '9acd32'
164
156
  }.freeze
165
157
 
166
- RGB_VALUE_REGEXP = "\s*(-?[0-9.]+%?)\s*"
167
- RGB_REGEXP = /\Argb\(#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP}\)\z/i
158
+ VALUE_REGEXP = "\s*(-?[0-9.]+%?)\s*"
159
+ RGB_REGEXP = /\Argb\(#{VALUE_REGEXP},#{VALUE_REGEXP},#{VALUE_REGEXP}\)\z/i
160
+ CMYK_REGEXP = /\Adevice-cmyk\(#{VALUE_REGEXP},#{VALUE_REGEXP},#{VALUE_REGEXP},#{VALUE_REGEXP}\)\z/i
168
161
  URL_REGEXP = /\Aurl\(([^)]*)\)\z/i
169
162
 
170
- def self.parse(color_string, gradients = nil)
163
+ def self.parse(color_string, gradients = nil, color_mode = :rgb)
171
164
  url_specified = false
172
165
 
173
166
  components = color_string.to_s.strip.scan(/([^(\s]+(\([^)]*\))?)/)
174
167
 
175
168
  result = components.map do |color, *_|
176
169
  if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i)
177
- Hex.new("#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}")
170
+ RGB.new("#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}")
178
171
 
179
172
  elsif color.match(/\A#[0-9a-f]{6}\z/i)
180
- Hex.new(color[1..6])
173
+ RGB.new(color[1..6])
181
174
 
182
175
  elsif hex = HTML_COLORS[color.downcase]
183
- Hex.new(hex)
176
+ hex_color(hex, color_mode)
184
177
 
185
178
  elsif m = color.match(RGB_REGEXP)
186
179
  hex = (1..3).collect do |n|
@@ -189,7 +182,16 @@ class Prawn::SVG::Color
189
182
  "%02x" % clamp(value.round, 0, 255)
190
183
  end.join
191
184
 
192
- Hex.new(hex)
185
+ RGB.new(hex)
186
+
187
+ elsif m = color.match(CMYK_REGEXP)
188
+ cmyk = (1..4).collect do |n|
189
+ value = m[n].to_f
190
+ value *= 100 unless m[n][-1..-1] == '%'
191
+ clamp(value, 0, 100)
192
+ end
193
+
194
+ CMYK.new(cmyk)
193
195
 
194
196
  elsif matches = color.match(URL_REGEXP)
195
197
  url_specified = true
@@ -203,18 +205,40 @@ class Prawn::SVG::Color
203
205
  # Generally, we default to black if the colour was unparseable.
204
206
  # http://www.w3.org/TR/SVG/painting.html section 11.2 says if a URL was
205
207
  # supplied without a fallback, that's an error.
206
- result << DEFAULT_COLOR unless url_specified
208
+ result << default_color(color_mode) unless url_specified
207
209
 
208
210
  result.compact
209
211
  end
210
212
 
211
- def self.color_to_hex(color)
212
- result = parse(color).detect {|result| result.is_a?(Hex)}
213
+ def self.css_color_to_prawn_color(color)
214
+ result = parse(color).detect {|result| result.is_a?(RGB) || result.is_a?(CMYK)}
213
215
  result.value if result
214
216
  end
215
217
 
216
218
  protected
219
+
217
220
  def self.clamp(value, min_value, max_value)
218
221
  [[value, min_value].max, max_value].min
219
222
  end
223
+
224
+ def self.default_color(color_mode)
225
+ color_mode == :cmyk ? CMYK_DEFAULT_COLOR : RGB_DEFAULT_COLOR
226
+ end
227
+
228
+ def self.hex_color(hex, color_mode)
229
+ if color_mode == :cmyk
230
+ r, g, b = [hex[0..1], hex[2..3], hex[4..5]].map { |h| h.to_i(16) / 255.0 }
231
+ k = 1 - [r, g, b].max
232
+ if k == 1
233
+ CMYK.new([0, 0, 0, 100])
234
+ else
235
+ c = (1 - r - k) / (1 - k)
236
+ m = (1 - g - k) / (1 - k)
237
+ y = (1 - b - k) / (1 - k)
238
+ CMYK.new([c, m, y, k].map { |v| (v * 100).round })
239
+ end
240
+ else
241
+ RGB.new(hex)
242
+ end
243
+ end
220
244
  end
@@ -10,7 +10,7 @@ module Prawn::SVG::CSS
10
10
  case token
11
11
  when Modifier
12
12
  part = token.type
13
- result.last[part] ||= part == :name ? "" : []
13
+ result.last[part] ||= part == :name ? +"" : []
14
14
  when Identifier
15
15
  return unless part
16
16
  result.last[part] << token.name
@@ -48,7 +48,7 @@ module Prawn::SVG::CSS
48
48
  case attribute
49
49
  when :pre_key
50
50
  if VALID_CSS_IDENTIFIER_CHAR.match(char)
51
- result.last.key = char
51
+ result.last.key = String.new(char)
52
52
  attribute = :key
53
53
  elsif char != " " && char != "\t"
54
54
  return
@@ -60,7 +60,7 @@ module Prawn::SVG::CSS
60
60
  elsif char == "]"
61
61
  attribute = nil
62
62
  elsif "=*~^|$".include?(char)
63
- result.last.operator = char
63
+ result.last.operator = String.new(char)
64
64
  attribute = :operator
65
65
  elsif char == " " || char == "\t"
66
66
  attribute = :pre_operator
@@ -70,7 +70,7 @@ module Prawn::SVG::CSS
70
70
 
71
71
  when :pre_operator
72
72
  if "=*~^|$".include?(char)
73
- result.last.operator = char
73
+ result.last.operator = String.new(char)
74
74
  attribute = :operator
75
75
  elsif char != " " && char != "\t"
76
76
  return
@@ -82,25 +82,25 @@ module Prawn::SVG::CSS
82
82
  elsif char == " " || char == "\t"
83
83
  attribute = :pre_value
84
84
  elsif char == '"' || char == "'"
85
- result.last.value = ''
86
- attribute = char
85
+ result.last.value = +''
86
+ attribute = String.new(char)
87
87
  else
88
- result.last.value = char
88
+ result.last.value = String.new(char)
89
89
  attribute = :value
90
90
  end
91
91
 
92
92
  when :pre_value
93
93
  if char == '"' || char == "'"
94
- result.last.value = ''
95
- attribute = char
94
+ result.last.value = +''
95
+ attribute = String.new(char)
96
96
  elsif char != " " && char != "\t"
97
- result.last.value = char
97
+ result.last.value = String.new(char)
98
98
  attribute = :value
99
99
  end
100
100
 
101
101
  when :value
102
102
  if char == "]"
103
- result.last.value = result.last.value.rstrip
103
+ result.last.value = String.new(result.last.value.rstrip)
104
104
  attribute = nil
105
105
  else
106
106
  result.last.value << char
@@ -69,14 +69,14 @@ module Prawn::SVG::CSS
69
69
 
70
70
  result = case element[:combinator]
71
71
  when :child
72
- "/"
72
+ +"/"
73
73
  when :adjacent
74
74
  pseudo_classes << 'first-child'
75
- "/following-sibling::"
75
+ +"/following-sibling::"
76
76
  when :siblings
77
- "/following-sibling::"
77
+ +"/following-sibling::"
78
78
  else
79
- "//"
79
+ +"//"
80
80
  end
81
81
 
82
82
  positions = []
@@ -13,7 +13,8 @@ class Prawn::SVG::Document
13
13
  :font_registry,
14
14
  :url_loader,
15
15
  :elements_by_id, :gradients,
16
- :element_styles
16
+ :element_styles,
17
+ :color_mode
17
18
 
18
19
  def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new)
19
20
  @root = REXML::Document.new(data).root
@@ -29,9 +30,10 @@ class Prawn::SVG::Document
29
30
  @warnings = []
30
31
  @options = options
31
32
  @elements_by_id = {}
32
- @gradients = {}
33
+ @gradients = Prawn::SVG::Gradients.new(self)
33
34
  @fallback_font_name = options.fetch(:fallback_font_name, DEFAULT_FALLBACK_FONT_NAME)
34
35
  @font_registry = font_registry
36
+ @color_mode = load_color_mode
35
37
 
36
38
  @url_loader = Prawn::SVG::UrlLoader.new(
37
39
  enable_cache: options[:cache_images],
@@ -52,4 +54,15 @@ class Prawn::SVG::Document
52
54
  sizing.requested_height = requested_height
53
55
  sizing.calculate
54
56
  end
57
+
58
+ private
59
+
60
+ def load_color_mode
61
+ case @options[:color_mode]
62
+ when nil, :rgb then :rgb
63
+ when :cmyk then :cmyk
64
+ else
65
+ raise ArgumentError, ":color_mode must be set to :rgb (default) or :cmyk"
66
+ end
67
+ end
55
68
  end
@@ -91,12 +91,12 @@ class Prawn::SVG::Elements::Base
91
91
  parse_xml_space_attribute
92
92
  end
93
93
 
94
- def add_call(name, *arguments)
95
- @calls << [name.to_s, arguments, []]
94
+ def add_call(name, *arguments, **kwarguments)
95
+ @calls << [name.to_s, arguments, kwarguments, []]
96
96
  end
97
97
 
98
- def add_call_and_enter(name, *arguments)
99
- @calls << [name.to_s, arguments, []]
98
+ def add_call_and_enter(name, *arguments, **kwarguments)
99
+ @calls << [name.to_s, arguments, kwarguments, []]
100
100
  @calls = @calls.last.last
101
101
  end
102
102
 
@@ -168,7 +168,7 @@ class Prawn::SVG::Elements::Base
168
168
  command = stroke ? 'fill_and_stroke' : 'fill'
169
169
 
170
170
  if computed_properties.fill_rule == 'evenodd'
171
- add_call_and_enter(command, {fill_rule: :even_odd})
171
+ add_call_and_enter(command, fill_rule: :even_odd)
172
172
  else
173
173
  add_call_and_enter(command)
174
174
  end
@@ -189,11 +189,11 @@ class Prawn::SVG::Elements::Base
189
189
  color = computed_properties.color
190
190
  end
191
191
 
192
- results = Prawn::SVG::Color.parse(color, document.gradients)
192
+ results = Prawn::SVG::Color.parse(color, document.gradients, document.color_mode)
193
193
 
194
194
  success = results.detect do |result|
195
195
  case result
196
- when Prawn::SVG::Color::Hex
196
+ when Prawn::SVG::Color::RGB, Prawn::SVG::Color::CMYK
197
197
  add_call "#{type}_color", result.value
198
198
  true
199
199
  when Prawn::SVG::Elements::Gradient
@@ -237,16 +237,15 @@ class Prawn::SVG::Elements::Base
237
237
 
238
238
  def parse_css_declarations(declarations)
239
239
  # copied from css_parser
240
- declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')
241
-
242
- output = {}
243
- declarations.split(/[\;$]+/m).each do |decs|
244
- if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
245
- property, value, _ = matches.captures
246
- output[property.downcase] = value
240
+ declarations
241
+ .gsub(/(^[\s]*)|([\s]*$)/, '')
242
+ .split(/[\;$]+/m)
243
+ .each_with_object({}) do |decs, output|
244
+ if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
245
+ property, value, _ = matches.captures
246
+ output[property.downcase] = value
247
+ end
247
248
  end
248
- end
249
- output
250
249
  end
251
250
 
252
251
  def require_attributes(*names)
@@ -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)
@@ -147,8 +147,8 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
147
147
  offset = result.last.first
148
148
  end
149
149
 
150
- if color_hex = Prawn::SVG::Color.color_to_hex(child.properties.stop_color)
151
- result << [offset, color_hex]
150
+ if color = Prawn::SVG::Color.css_color_to_prawn_color(child.properties.stop_color)
151
+ result << [offset, color]
152
152
  end
153
153
  end
154
154
 
@@ -67,15 +67,14 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
67
67
  protected
68
68
 
69
69
  def image_dimensions(data)
70
- handler = if data[0, 3].unpack("C*") == [255, 216, 255]
71
- Prawn::Images::JPG
72
- elsif data[0, 8].unpack("C*") == [137, 80, 78, 71, 13, 10, 26, 10]
73
- Prawn::Images::PNG
74
- else
75
- raise SkipElementError, "Unsupported image type supplied to image tag; Prawn only supports JPG and PNG"
70
+ unless (handler = find_image_handler(data))
71
+ raise SkipElementError, 'Unsupported image type supplied to image tag'
76
72
  end
77
-
78
73
  image = handler.new(data)
79
74
  [image.width.to_f, image.height.to_f]
80
75
  end
76
+
77
+ def find_image_handler(data)
78
+ Prawn.image_handler.find(data) rescue nil
79
+ end
81
80
  end
@@ -55,8 +55,6 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
55
55
  end
56
56
 
57
57
  def apply
58
- add_call 'join_style', :bevel
59
-
60
58
  apply_commands
61
59
  apply_markers
62
60
  end
@@ -9,7 +9,7 @@ class Prawn::SVG::Elements::Root < Prawn::SVG::Elements::Base
9
9
 
10
10
  def apply
11
11
  if [nil, 'inherit', 'none', 'currentColor'].include?(properties.fill)
12
- add_call 'fill_color', '000000'
12
+ add_call 'fill_color', Prawn::SVG::Color.default_color(@document.color_mode).value
13
13
  end
14
14
 
15
15
  add_call 'transformation_matrix', @document.sizing.x_scale, 0, 0, @document.sizing.y_scale, 0, 0
@@ -41,14 +41,15 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
41
41
  font = select_font
42
42
  apply_font(font) if font
43
43
 
44
- # text_anchor isn't a Prawn option; we have to do some math to support it
45
- # and so we handle this in Prawn::SVG::Interface#rewrite_call_arguments
44
+ # text_anchor and dominant_baseline aren't Prawn options; we have to do some math to support them
45
+ # and so we handle them in Prawn::SVG::Interface#rewrite_call_arguments
46
46
  opts = {
47
47
  size: computed_properties.numerical_font_size,
48
48
  style: font && font.subfamily,
49
49
  text_anchor: computed_properties.text_anchor,
50
50
  }
51
51
 
52
+ opts[:dominant_baseline] = computed_properties.dominant_baseline unless computed_properties.dominant_baseline == 'auto'
52
53
  opts[:decoration] = computed_properties.text_decoration unless computed_properties.text_decoration == 'none'
53
54
 
54
55
  if state.text.parent
@@ -146,10 +147,10 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
146
147
  end
147
148
 
148
149
  if remaining
149
- add_call 'draw_text', text[0..0], opts.dup
150
+ add_call 'draw_text', text[0..0], **opts.dup
150
151
  text = text[1..-1]
151
152
  else
152
- add_call 'draw_text', text, opts.dup
153
+ add_call 'draw_text', text, **opts.dup
153
154
 
154
155
  # we can get to this path with rotations still pending
155
156
  # solve this by shifting them out by the number of
@@ -171,7 +172,11 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
171
172
 
172
173
  def svg_text_children
173
174
  text_children.select do |child|
174
- child.node_type == :text || child.namespace == SVG_NAMESPACE || child.namespace == ''
175
+ child.node_type == :text || (
176
+ child.node_type == :element && (
177
+ child.namespace == SVG_NAMESPACE || child.namespace == ''
178
+ )
179
+ )
175
180
  end
176
181
  end
177
182
 
@@ -0,0 +1,48 @@
1
+ module Prawn::SVG
2
+ class Gradients
3
+ def initialize(document)
4
+ @document = document
5
+ @gradients_by_id = {}
6
+ end
7
+
8
+ def [](id)
9
+ id &&= id.strip
10
+ return unless id && id != ''
11
+
12
+ if element = @gradients_by_id[id]
13
+ element
14
+ elsif raw_element = find_raw_gradient_element_by_id(id)
15
+ create_gradient_element(raw_element)
16
+ end
17
+ end
18
+
19
+ def []=(id, gradient)
20
+ @gradients_by_id[id] = gradient
21
+ end
22
+
23
+ private
24
+
25
+ def find_raw_gradient_element_by_id(id)
26
+ raw_element = find_raw_element_by_id(id)
27
+ raw_element if gradient_element?(raw_element)
28
+ end
29
+
30
+ def create_gradient_element(raw_element)
31
+ Elements::Gradient.new(@document, raw_element, [], new_state).tap(&:process)
32
+ end
33
+
34
+ def find_raw_element_by_id(id)
35
+ REXML::XPath.match(@document.root, %(//*[@id="#{id.gsub('"', '\"')}"])).first
36
+ end
37
+
38
+ def gradient_element?(raw_element)
39
+ Elements::TAG_CLASS_MAPPING[raw_element.name.to_sym] == Elements::Gradient
40
+ end
41
+
42
+ def new_state
43
+ State.new.tap do |state|
44
+ state.viewport_sizing = @document.sizing
45
+ end
46
+ end
47
+ end
48
+ end
@@ -5,7 +5,7 @@
5
5
  module Prawn
6
6
  module SVG
7
7
  class Interface
8
- VALID_OPTIONS = [:at, :position, :vposition, :width, :height, :cache_images, :enable_web_requests, :enable_file_requests_with_root, :fallback_font_name]
8
+ VALID_OPTIONS = [:at, :position, :vposition, :width, :height, :cache_images, :enable_web_requests, :enable_file_requests_with_root, :fallback_font_name, :color_mode]
9
9
 
10
10
  attr_reader :data, :prawn, :document, :options
11
11
 
@@ -109,10 +109,10 @@ 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
@@ -120,27 +120,42 @@ module Prawn
120
120
  if skip
121
121
  # the call has been overridden
122
122
  elsif children.empty? && call != 'transparent' # some prawn calls complain if they aren't supplied a block
123
- prawn.send(call, *arguments)
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
 
141
149
  at[0] = @cursor[0] if at[0] == :relative
142
150
  at[1] = @cursor[1] if at[1] == :relative
143
151
 
152
+ case options.delete(:dominant_baseline)
153
+ when 'middle'
154
+ height = prawn.font.height
155
+ at[1] -= height / 2.0
156
+ @cursor = [at[0], at[1]]
157
+ end
158
+
144
159
  if offset = options.delete(:offset)
145
160
  at[0] += offset[0]
146
161
  at[1] -= offset[1]
@@ -208,7 +223,7 @@ module Prawn
208
223
  # prawn (as at 2.0.1 anyway) uses 'b' for its fill_and_stroke. 'b' is 'h' (closepath) + 'B', and we
209
224
  # never want closepath to be automatically run as it stuffs up many drawing operations, such as dashes
210
225
  # and line caps, and makes paths close that we didn't ask to be closed when fill is specified.
211
- even_odd = arguments[0].is_a?(Hash) && arguments[0][:fill_rule] == :even_odd
226
+ even_odd = kwarguments[:fill_rule] == :even_odd
212
227
  content = even_odd ? 'B*' : 'B'
213
228
  prawn.add_content content
214
229