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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +35 -0
- data/README.md +8 -6
- data/lib/prawn/svg/attributes/stroke.rb +5 -0
- data/lib/prawn/svg/color.rb +46 -22
- data/lib/prawn/svg/css/selector_parser.rb +11 -11
- data/lib/prawn/svg/css/stylesheets.rb +4 -4
- data/lib/prawn/svg/document.rb +15 -2
- data/lib/prawn/svg/elements/base.rb +15 -16
- data/lib/prawn/svg/elements/call_duplicator.rb +1 -1
- data/lib/prawn/svg/elements/gradient.rb +2 -2
- data/lib/prawn/svg/elements/image.rb +6 -7
- data/lib/prawn/svg/elements/path.rb +0 -2
- data/lib/prawn/svg/elements/root.rb +1 -1
- data/lib/prawn/svg/elements/text_component.rb +10 -5
- data/lib/prawn/svg/gradients.rb +48 -0
- data/lib/prawn/svg/interface.rb +23 -8
- data/lib/prawn/svg/properties.rb +2 -0
- data/lib/prawn/svg/version.rb +1 -1
- data/lib/prawn-svg.rb +1 -0
- data/prawn-svg.gemspec +17 -15
- data/spec/integration_spec.rb +24 -24
- data/spec/prawn/svg/color_spec.rb +29 -16
- data/spec/prawn/svg/css/stylesheets_spec.rb +1 -19
- data/spec/prawn/svg/document_spec.rb +17 -0
- data/spec/prawn/svg/elements/base_spec.rb +15 -15
- data/spec/prawn/svg/elements/line_spec.rb +12 -12
- data/spec/prawn/svg/elements/marker_spec.rb +27 -27
- data/spec/prawn/svg/elements/polygon_spec.rb +9 -9
- data/spec/prawn/svg/elements/polyline_spec.rb +7 -7
- data/spec/prawn/svg/elements/root_spec.rb +28 -0
- data/spec/prawn/svg/elements/text_spec.rb +80 -53
- data/spec/prawn/svg/interface_spec.rb +10 -2
- data/spec/prawn/svg/pathable_spec.rb +4 -4
- data/spec/sample_svg/cmyk.svg +5 -0
- data/spec/sample_svg/gradients-cmyk.svg +40 -0
- data/spec/sample_svg/ordering.svg +12 -0
- data/spec/spec_helper.rb +2 -2
- metadata +53 -16
- data/.travis.yml +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9f71b82858f8ee91e1ccaf2903c2e2a0d49108b1532a81d15c2275362a0d136
|
4
|
+
data.tar.gz: f3336c6a727df24e6a5877e4d26cf030eabbb1c9af9852f95c35c5bfe5128995
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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.
|
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><svg></tt>, <tt><g></tt> and <tt><symbol></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><style></tt> (see CSS section below)
|
71
72
|
|
72
|
-
-
|
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><clipPath></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
|
data/lib/prawn/svg/color.rb
CHANGED
@@ -1,17 +1,9 @@
|
|
1
1
|
class Prawn::SVG::Color
|
2
|
-
|
3
|
-
|
2
|
+
RGB = Struct.new(:value)
|
3
|
+
CMYK = Struct.new(:value)
|
4
4
|
|
5
|
-
|
6
|
-
|
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
|
-
|
167
|
-
RGB_REGEXP = /\Argb\(#{
|
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
|
-
|
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
|
-
|
173
|
+
RGB.new(color[1..6])
|
181
174
|
|
182
175
|
elsif hex = HTML_COLORS[color.downcase]
|
183
|
-
|
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
|
-
|
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 <<
|
208
|
+
result << default_color(color_mode) unless url_specified
|
207
209
|
|
208
210
|
result.compact
|
209
211
|
end
|
210
212
|
|
211
|
-
def self.
|
212
|
-
result = parse(color).detect {|result| result.is_a?(
|
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 = []
|
data/lib/prawn/svg/document.rb
CHANGED
@@ -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,
|
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::
|
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
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
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]),
|
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
|
151
|
-
result << [offset,
|
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 =
|
71
|
-
|
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
|
@@ -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',
|
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
|
45
|
-
# and so we handle
|
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 ||
|
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
|
data/lib/prawn/svg/interface.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 =
|
226
|
+
even_odd = kwarguments[:fill_rule] == :even_odd
|
212
227
|
content = even_odd ? 'B*' : 'B'
|
213
228
|
prawn.add_content content
|
214
229
|
|