prawn-svg 0.32.0 → 0.34.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +20 -3
  3. data/.gitignore +1 -0
  4. data/README.md +32 -24
  5. data/lib/prawn/svg/attributes/stroke.rb +5 -0
  6. data/lib/prawn/svg/color.rb +46 -22
  7. data/lib/prawn/svg/css/selector_parser.rb +11 -11
  8. data/lib/prawn/svg/css/stylesheets.rb +4 -4
  9. data/lib/prawn/svg/document.rb +20 -4
  10. data/lib/prawn/svg/elements/base.rb +10 -11
  11. data/lib/prawn/svg/elements/gradient.rb +2 -2
  12. data/lib/prawn/svg/elements/image.rb +54 -21
  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 +12 -3
  16. data/lib/prawn/svg/elements/use.rb +20 -12
  17. data/lib/prawn/svg/elements.rb +1 -0
  18. data/lib/prawn/svg/gradients.rb +48 -0
  19. data/lib/prawn/svg/interface.rb +17 -187
  20. data/lib/prawn/svg/loaders/data.rb +4 -3
  21. data/lib/prawn/svg/properties.rb +2 -0
  22. data/lib/prawn/svg/renderer.rb +236 -0
  23. data/lib/prawn/svg/version.rb +1 -1
  24. data/lib/prawn-svg.rb +2 -0
  25. data/prawn-svg.gemspec +18 -16
  26. data/spec/prawn/svg/color_spec.rb +29 -16
  27. data/spec/prawn/svg/document_spec.rb +17 -0
  28. data/spec/prawn/svg/elements/marker_spec.rb +1 -1
  29. data/spec/prawn/svg/elements/root_spec.rb +28 -0
  30. data/spec/prawn/svg/elements/text_spec.rb +27 -0
  31. data/spec/prawn/svg/interface_spec.rb +10 -2
  32. data/spec/sample_images/image_svg_embed.svg +6 -0
  33. data/spec/sample_svg/cmyk.svg +5 -0
  34. data/spec/sample_svg/gradients-cmyk.svg +40 -0
  35. data/spec/sample_svg/image_svg.svg +9 -0
  36. data/spec/sample_svg/ordering.svg +12 -0
  37. data/spec/sample_svg/symbol.svg +12 -0
  38. data/spec/spec_helper.rb +29 -5
  39. metadata +67 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d054ded3422dc4cd1cb870dc09549c0a82c1e2813e314565c7a3aec70d95d188
4
- data.tar.gz: 123a00b97f78f284a8e11fad37fed42cbec06d678675ec15ff7dafbcfe6505d5
3
+ metadata.gz: fa5a995fd0ec6d8426dcc87603695e91ff743e1fddc559da6cc7fcc08ab5eb88
4
+ data.tar.gz: d76a03c53c2d7dab41db812e3309f59f32baefb730970015a8bb35d6fb791653
5
5
  SHA512:
6
- metadata.gz: 4ece284ced16e7b6f4d7562c17a97780375f4e255eb4199d489cfb1fa473dc1f65e6b9fa48894079f0756b42a749cc58f34c00507e19ccdfddab55723c82311d
7
- data.tar.gz: 8826f6f457372cc7df38869e6448974f5731fdee1040bca44b8446b71c57e0713340481a3400705c8fd11ec5d13959dcd99f24a0fe8820c89a37ca725545656c
6
+ metadata.gz: 0a686e69f4759243fc9b672a4bad663f17bf3f4f1a7016e560d816859d3d49464dfbb6416d877cf35f5362cd316111661e96817b645f707cc19999bae7e98f17
7
+ data.tar.gz: 57fbf5408c56228a286ca10221759440c4fc1326c0855acc4c5a74a30652ca59af277d170cf5bcb7f40b24bfe511e6ff82fd24e81da301e15595dc7f9d283e7a
@@ -1,14 +1,31 @@
1
1
  name: test
2
2
  on: [push, pull_request]
3
3
  jobs:
4
- rake:
4
+ rake_new:
5
5
  runs-on: ubuntu-latest
6
6
  strategy:
7
7
  fail-fast: false
8
8
  matrix:
9
- ruby: [2.3, 2.4, 2.5, 2.6, 2.7, '3.0']
9
+ ruby: [2.6, 2.7, '3.0', 3.1, 3.2, 3.3]
10
10
  steps:
11
- - uses: actions/checkout@v2
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
12
29
  - name: Set up Ruby
13
30
  uses: ruby/setup-ruby@v1
14
31
  with:
data/.gitignore CHANGED
@@ -7,3 +7,4 @@ Gemfile.lock
7
7
  .ruby-version
8
8
  vendor/bundle/
9
9
  .bundle/
10
+ .mise.toml
data/README.md CHANGED
@@ -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.3.0.
13
+ The minimum Ruby version required is 2.5.0.
14
14
 
15
15
  ## Using prawn-svg
16
16
 
@@ -33,7 +33,8 @@ Option | Data type | Description
33
33
  :enable_web_requests | boolean | If true, prawn-svg will make http and https requests to fetch images. Defaults to true.
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
- :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.
36
+ :fallback_font_name | string | A font name which will override the default fallback font of Times-Roman. If this value is set to `nil`, 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
 
@@ -53,51 +54,57 @@ Option | Data type | Description
53
54
 
54
55
  prawn-svg supports most but not all of the full SVG 1.1 specification. It currently supports:
55
56
 
56
- - <tt>&lt;line&gt;</tt>, <tt>&lt;polyline&gt;</tt>, <tt>&lt;polygon&gt;</tt>, <tt>&lt;circle&gt;</tt> and <tt>&lt;ellipse&gt;</tt>
57
+ - `<line>`, `<polyline>`, `<polygon>`, `<circle>` and `<ellipse>`
57
58
 
58
- - <tt>&lt;rect&gt;</tt>. Rounded rects are supported, but only one radius is applied to all corners.
59
+ - `<rect>`. Rounded rects are supported, but only one radius is applied to all corners.
59
60
 
60
- - <tt>&lt;path&gt;</tt> supports all commands defined in SVG 1.1, although the
61
+ - `<path>` supports all commands defined in SVG 1.1, although the
61
62
  implementation of elliptical arc is a bit rough at the moment.
62
63
 
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`
64
+ - `<text>`, `<tspan>` and `<tref>` with attributes `x`, `y`, `dx`, `dy`, `rotate`, `textLength`, `lengthAdjust`,
65
+ and with extra properties `text-anchor`, `text-decoration` (underline only), `font-size`, `font-family`,
66
+ `font-weight`, `font-style`, `letter-spacing`, `dominant-baseline` (middle only)
65
67
 
66
- - <tt>&lt;svg&gt;</tt>, <tt>&lt;g&gt;</tt> and <tt>&lt;symbol&gt;</tt>
68
+ - `<svg>`, `<g>` and `<symbol>`
67
69
 
68
- - <tt>&lt;use&gt;</tt>
70
+ - `<use>`
69
71
 
70
- - <tt>&lt;style&gt;</tt> (see CSS section below)
72
+ - `<style>` (see CSS section below)
71
73
 
72
- - <tt>&lt;image&gt;</tt> with <tt>http:</tt>, <tt>https:</tt>, <tt>data:image/\*;base64</tt> and `file:` schemes
73
- (`file:` is disabled by default for security reasons, see Options section above)
74
+ - `<image>` referencing a JPEG, PNG, or SVG image, with `http:`, `https:`, `data:image/jpeg;base64`,
75
+ `data:image/png;base64`, `data:image/svg+xml;base64` and `file:` schemes (`file:` is disabled by default for
76
+ security reasons, see Options section above)
74
77
 
75
- - <tt>&lt;clipPath&gt;</tt>
78
+ - `<clipPath>`
76
79
 
77
80
  - `<marker>`
78
81
 
79
- - `<linearGradient>` and `<radialGradient>` are implemented on Prawn 2.2.0+ with attributes `gradientUnits` and `gradientTransform` (spreadMethod and stop-opacity are unimplemented.)
82
+ - `<linearGradient>` and `<radialGradient>` are implemented on Prawn 2.2.0+ with attributes `gradientUnits` and
83
+ `gradientTransform` (`spreadMethod` and `stop-opacity` are unimplemented.)
80
84
 
81
85
  - `<switch>` and `<foreignObject>`, although prawn-svg cannot handle any data that is not SVG so `<foreignObject>`
82
86
  tags are always ignored.
83
87
 
84
- - properties: `clip-path`, `color`, `display`, `fill`, `fill-opacity`, `fill-rule`, `opacity`, `overflow`, `stroke`, `stroke-dasharray`, `stroke-linecap`, `stroke-opacity`, `stroke-width`
88
+ - properties: `clip-path`, `color`, `display`, `fill`, `fill-opacity`, `fill-rule`, `opacity`, `overflow`,
89
+ `stroke`, `stroke-dasharray`, `stroke-linecap`, `stroke-linejoin`, `stroke-opacity`, `stroke-width`
85
90
 
86
91
  - properties on lines, polylines, polygons and paths: `marker-end`, `marker-mid`, `marker-start`
87
92
 
88
93
  - attributes on all elements: `class`, `id`, `style`, `transform`, `xml:space`
89
94
 
90
- - the <tt>viewBox</tt> attribute on <tt>&lt;svg&gt;</tt> and `<marker>` elements
95
+ - the `viewBox` attribute on `<svg>` and `<marker>` elements
91
96
 
92
- - the <tt>preserveAspectRatio</tt> attribute on <tt>&lt;svg&gt;</tt>, <tt>&lt;image&gt;</tt> and `<marker>` elements
97
+ - the `preserveAspectRatio` attribute on `<svg>`, `<image>` and `<marker>` elements
93
98
 
94
99
  - transform methods: `translate`, `translateX`, `translateY`, `rotate`, `scale`, `skewX`, `skewY`, `matrix`
95
100
 
96
- - colors: HTML standard names, <tt>#xxx</tt>, <tt>#xxxxxx</tt>, <tt>rgb(1, 2, 3)</tt>, <tt>rgb(1%, 2%, 3%)</tt>
101
+ - colors: HTML standard names, `#xxx`, `#xxxxxx`, `rgb(1, 2, 3)`, `rgb(1%, 2%, 3%)`, and also the non-standard
102
+ `device-cmyk(1, 2, 3, 4)` for CMYK colors
97
103
 
98
- - 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>
104
+ - measurements specified in `pt`, `cm`, `dm`, `ft`, `in`, `m`, `mm`, `yd`, `pc`, `%`
99
105
 
100
- - fonts: generic CSS fonts, built-in PDF fonts, and any TTF fonts in your fonts path, specified in any of the measurements above plus `em` or `rem`
106
+ - fonts: generic CSS fonts, built-in PDF fonts, and any TTF fonts in your fonts path, specified in any of the
107
+ measurements above plus `em` or `rem`
101
108
 
102
109
  ## CSS
103
110
 
@@ -116,7 +123,7 @@ implemented, but `!important` is not.
116
123
 
117
124
  ## Not supported
118
125
 
119
- prawn-svg does not support hyperlinks, patterns or filters.
126
+ prawn-svg does not support hyperlinks, patterns, masks or filters.
120
127
 
121
128
  It does not support text in the clip area, but you can clip shapes and text by any shape.
122
129
 
@@ -124,8 +131,9 @@ It does not support text in the clip area, but you can clip shapes and text by a
124
131
 
125
132
  ### Fonts
126
133
 
127
- By default, prawn-svg has a fonts path of <tt>["/Library/Fonts", "/System/Library/Fonts", "#{ENV["HOME"]}/Library/Fonts", "/usr/share/fonts/truetype"]</tt> to catch
128
- Mac OS X and Debian Linux users. You can add to the font path:
134
+ By default, prawn-svg has a fonts path of `["/Library/Fonts", "/System/Library/Fonts",
135
+ "#{ENV["HOME"]}/Library/Fonts", "/usr/share/fonts/truetype"]` to catch MacOS and Debian Linux users. You can add
136
+ to the font path:
129
137
 
130
138
  ```ruby
131
139
  Prawn::SVG::FontRegistry.font_path << "/my/font/directory"
@@ -137,4 +145,4 @@ In your Gemfile, put `gem 'prawn-svg'` before `gem 'prawn-rails'` so that prawn-
137
145
 
138
146
  ## Licence
139
147
 
140
- MIT licence. Copyright Roger Nesbitt.
148
+ MIT licence. Copyright Mog Nesbitt.
@@ -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,9 +13,10 @@ 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
- def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new)
19
+ def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new, attribute_overrides: {})
19
20
  @root = REXML::Document.new(data).root
20
21
 
21
22
  if @root.nil?
@@ -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],
@@ -39,7 +41,10 @@ class Prawn::SVG::Document
39
41
  enable_file_with_root: options[:enable_file_requests_with_root]
40
42
  )
41
43
 
42
- @sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, @root.attributes)
44
+ attributes = @root.attributes.dup
45
+ attribute_overrides.each { |key, value| attributes.add(REXML::Attribute.new(key, value)) }
46
+
47
+ @sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, attributes)
43
48
  calculate_sizing(requested_width: options[:width], requested_height: options[:height])
44
49
 
45
50
  @element_styles = Prawn::SVG::CSS::Stylesheets.new(css_parser, root).load
@@ -52,4 +57,15 @@ class Prawn::SVG::Document
52
57
  sizing.requested_height = requested_height
53
58
  sizing.calculate
54
59
  end
60
+
61
+ private
62
+
63
+ def load_color_mode
64
+ case @options[:color_mode]
65
+ when nil, :rgb then :rgb
66
+ when :cmyk then :cmyk
67
+ else
68
+ raise ArgumentError, ":color_mode must be set to :rgb (default) or :cmyk"
69
+ end
70
+ end
55
71
  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)
@@ -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
 
@@ -3,29 +3,32 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
3
3
  def initialize(data)
4
4
  @data = data
5
5
  end
6
+
6
7
  def read
7
8
  @data
8
9
  end
9
- def rewind
10
- end
10
+
11
+ def rewind; end
11
12
  end
12
13
 
14
+ ImageData = Struct.new(:dimensions, :document)
15
+
13
16
  def parse
14
17
  require_attributes 'width', 'height'
15
18
 
16
- raise SkipElementQuietly if state.computed_properties.display == "none"
19
+ raise SkipElementQuietly if state.computed_properties.display == 'none'
17
20
 
18
21
  @url = href_attribute
19
- if @url.nil?
20
- raise SkipElementError, "image tag must have an href or xlink:href"
21
- end
22
+ raise SkipElementError, 'image tag must have an href or xlink:href' if @url.nil?
22
23
 
23
24
  x = x(attributes['x'] || 0)
24
25
  y = y(attributes['y'] || 0)
25
26
  width = x_pixels(attributes['width'])
26
27
  height = y_pixels(attributes['height'])
28
+ preserveAspectRatio = attributes['preserveAspectRatio']
27
29
 
28
30
  raise SkipElementQuietly if width.zero? || height.zero?
31
+
29
32
  require_positive_value width, height
30
33
 
31
34
  @image = begin
@@ -34,7 +37,9 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
34
37
  raise SkipElementError, "Error retrieving URL #{@url}: #{e.message}"
35
38
  end
36
39
 
37
- @aspect = Prawn::SVG::Calculators::AspectRatio.new(attributes['preserveAspectRatio'], [width, height], image_dimensions(@image))
40
+ @image_data = process_image(@image, width, height, preserveAspectRatio)
41
+
42
+ @aspect = Prawn::SVG::Calculators::AspectRatio.new(preserveAspectRatio, [width, height], @image_data.dimensions)
38
43
 
39
44
  @clip_x = x
40
45
  @clip_y = y
@@ -49,15 +54,21 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
49
54
 
50
55
  def apply
51
56
  if @aspect.slice?
52
- add_call "save"
53
- add_call "rectangle", [@clip_x, @clip_y], @clip_width, @clip_height
54
- add_call "clip"
57
+ add_call 'save'
58
+ add_call 'rectangle', [@clip_x, @clip_y], @clip_width, @clip_height
59
+ add_call 'clip'
55
60
  end
56
61
 
57
- options = {:width => @width, :height => @height, :at => [@x, @y]}
62
+ if (document = @image_data.document)
63
+ add_call_and_enter 'translate', @x, @y
64
+ add_call 'svg:render_sub_document', document
65
+ else
66
+ options = { width: @width, height: @height, at: [@x, @y] }
67
+
68
+ add_call 'image', FakeIO.new(@image), options
69
+ end
58
70
 
59
- add_call "image", FakeIO.new(@image), options
60
- add_call "restore" if @aspect.slice?
71
+ add_call 'restore' if @aspect.slice?
61
72
  end
62
73
 
63
74
  def bounding_box
@@ -66,16 +77,38 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
66
77
 
67
78
  protected
68
79
 
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
80
+ def process_image(data, width, height, preserveAspectRatio)
81
+ if (handler = find_image_handler(data))
82
+ image = handler.new(data)
83
+ ImageData.new([image.width.to_f, image.height.to_f], nil)
84
+
85
+ elsif potentially_svg?(data)
86
+ document = Prawn::SVG::Document.new(
87
+ data, [width, height], { width: width, height: height },
88
+ attribute_overrides: { 'preserveAspectRatio' => preserveAspectRatio }
89
+ )
90
+
91
+ dimensions = [document.sizing.output_width, document.sizing.output_height]
92
+ ImageData.new(dimensions, document)
93
+
74
94
  else
75
- raise SkipElementError, "Unsupported image type supplied to image tag; Prawn only supports JPG and PNG"
95
+ raise_invalid_image_type
76
96
  end
97
+ rescue Prawn::SVG::Document::InvalidSVGData
98
+ raise_invalid_image_type
99
+ end
100
+
101
+ def find_image_handler(data)
102
+ Prawn.image_handler.find(data)
103
+ rescue StandardError
104
+ nil
105
+ end
106
+
107
+ def potentially_svg?(data)
108
+ data.include?('<svg')
109
+ end
77
110
 
78
- image = handler.new(data)
79
- [image.width.to_f, image.height.to_f]
111
+ def raise_invalid_image_type
112
+ raise SkipElementError, 'Unsupported image type supplied to image tag'
80
113
  end
81
114
  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