prawn-svg 0.27.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.travis.yml +6 -4
- data/LICENSE +1 -1
- data/README.md +23 -9
- data/lib/prawn-svg.rb +7 -1
- data/lib/prawn/svg/attributes/opacity.rb +4 -4
- data/lib/prawn/svg/attributes/transform.rb +2 -44
- data/lib/prawn/svg/calculators/document_sizing.rb +2 -2
- data/lib/prawn/svg/{css.rb → css/font_family_parser.rb} +3 -4
- data/lib/prawn/svg/css/selector_parser.rb +174 -0
- data/lib/prawn/svg/css/stylesheets.rb +146 -0
- data/lib/prawn/svg/document.rb +3 -15
- data/lib/prawn/svg/elements.rb +4 -2
- data/lib/prawn/svg/elements/base.rb +26 -23
- data/lib/prawn/svg/elements/clip_path.rb +12 -0
- data/lib/prawn/svg/elements/container.rb +1 -3
- data/lib/prawn/svg/elements/gradient.rb +83 -25
- data/lib/prawn/svg/elements/image.rb +2 -2
- data/lib/prawn/svg/elements/path.rb +42 -29
- data/lib/prawn/svg/elements/root.rb +4 -1
- data/lib/prawn/svg/elements/text.rb +1 -1
- data/lib/prawn/svg/elements/text_component.rb +63 -14
- data/lib/prawn/svg/elements/use.rb +23 -7
- data/lib/prawn/svg/extensions/additional_gradient_transforms.rb +23 -0
- data/lib/prawn/svg/font_registry.rb +4 -3
- data/lib/prawn/svg/interface.rb +26 -2
- data/lib/prawn/svg/loaders/data.rb +1 -1
- data/lib/prawn/svg/loaders/file.rb +4 -2
- data/lib/prawn/svg/properties.rb +2 -0
- data/lib/prawn/svg/state.rb +6 -3
- data/lib/prawn/svg/transform_parser.rb +72 -0
- data/lib/prawn/svg/version.rb +1 -1
- data/prawn-svg.gemspec +3 -4
- data/spec/prawn/svg/attributes/opacity_spec.rb +85 -0
- data/spec/prawn/svg/attributes/transform_spec.rb +30 -35
- data/spec/prawn/svg/calculators/document_sizing_spec.rb +4 -4
- data/spec/prawn/svg/{css_spec.rb → css/font_family_parser_spec.rb} +3 -3
- data/spec/prawn/svg/css/selector_parser_spec.rb +33 -0
- data/spec/prawn/svg/css/stylesheets_spec.rb +142 -0
- data/spec/prawn/svg/document_spec.rb +0 -33
- data/spec/prawn/svg/elements/base_spec.rb +58 -2
- data/spec/prawn/svg/elements/gradient_spec.rb +79 -4
- data/spec/prawn/svg/elements/path_spec.rb +29 -17
- data/spec/prawn/svg/elements/text_spec.rb +74 -16
- data/spec/prawn/svg/font_registry_spec.rb +30 -0
- data/spec/prawn/svg/interface_spec.rb +33 -1
- data/spec/prawn/svg/loaders/data_spec.rb +8 -0
- data/spec/prawn/svg/transform_parser_spec.rb +94 -0
- data/spec/sample_output/{directory → .keep} +0 -0
- data/spec/sample_svg/double_opacity.svg +6 -0
- data/spec/sample_svg/gradient_transform.svg +19 -0
- data/spec/sample_svg/links.svg +18 -0
- data/spec/sample_svg/radgrad01-bounding.svg +26 -0
- data/spec/sample_svg/radgrad01.svg +26 -0
- data/spec/sample_svg/svg_fill.svg +5 -0
- data/spec/sample_svg/text-decoration.svg +4 -0
- data/spec/sample_svg/text_stroke.svg +41 -0
- data/spec/sample_svg/transform.svg +20 -0
- data/spec/sample_svg/use_disordered.svg +17 -0
- data/spec/sample_svg/warning-radioactive.svg +98 -0
- data/spec/spec_helper.rb +2 -2
- metadata +137 -15
data/lib/prawn/svg/document.rb
CHANGED
@@ -12,7 +12,8 @@ class Prawn::SVG::Document
|
|
12
12
|
:fallback_font_name,
|
13
13
|
:font_registry,
|
14
14
|
:url_loader,
|
15
|
-
:
|
15
|
+
:elements_by_id, :gradients,
|
16
|
+
:element_styles
|
16
17
|
|
17
18
|
def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new)
|
18
19
|
@root = REXML::Document.new(data).root
|
@@ -31,7 +32,6 @@ class Prawn::SVG::Document
|
|
31
32
|
@gradients = {}
|
32
33
|
@fallback_font_name = options.fetch(:fallback_font_name, DEFAULT_FALLBACK_FONT_NAME)
|
33
34
|
@font_registry = font_registry
|
34
|
-
@css_parser = css_parser
|
35
35
|
|
36
36
|
@url_loader = Prawn::SVG::UrlLoader.new(
|
37
37
|
enable_cache: options[:cache_images],
|
@@ -42,7 +42,7 @@ class Prawn::SVG::Document
|
|
42
42
|
@sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, @root.attributes)
|
43
43
|
calculate_sizing(requested_width: options[:width], requested_height: options[:height])
|
44
44
|
|
45
|
-
|
45
|
+
@element_styles = Prawn::SVG::CSS::Stylesheets.new(css_parser, root).load
|
46
46
|
|
47
47
|
yield self if block_given?
|
48
48
|
end
|
@@ -52,16 +52,4 @@ class Prawn::SVG::Document
|
|
52
52
|
sizing.requested_height = requested_height
|
53
53
|
sizing.calculate
|
54
54
|
end
|
55
|
-
|
56
|
-
private
|
57
|
-
|
58
|
-
# <style> elements specified anywhere in the document apply to the entire
|
59
|
-
# document. Because of this, we load all <style> elements before parsing
|
60
|
-
# the rest of the document.
|
61
|
-
def parse_style_elements
|
62
|
-
REXML::XPath.match(root, '//style').each do |source|
|
63
|
-
data = source.texts.map(&:value).join
|
64
|
-
css_parser.add_block!(data)
|
65
|
-
end
|
66
|
-
end
|
67
55
|
end
|
data/lib/prawn/svg/elements.rb
CHANGED
@@ -4,7 +4,7 @@ end
|
|
4
4
|
|
5
5
|
require 'prawn/svg/elements/call_duplicator'
|
6
6
|
|
7
|
-
%w(base depth_first_base root container viewport text text_component line polyline polygon circle ellipse rect path use image gradient marker ignored).each do |filename|
|
7
|
+
%w(base depth_first_base root container clip_path viewport text text_component line polyline polygon circle ellipse rect path use image gradient marker ignored).each do |filename|
|
8
8
|
require "prawn/svg/elements/#{filename}"
|
9
9
|
end
|
10
10
|
|
@@ -13,7 +13,8 @@ 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
|
-
|
16
|
+
a: Prawn::SVG::Elements::Container,
|
17
|
+
clipPath: Prawn::SVG::Elements::ClipPath,
|
17
18
|
switch: Prawn::SVG::Elements::Container,
|
18
19
|
svg: Prawn::SVG::Elements::Viewport,
|
19
20
|
text: Prawn::SVG::Elements::Text,
|
@@ -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"
|
@@ -157,14 +159,23 @@ class Prawn::SVG::Elements::Base
|
|
157
159
|
end
|
158
160
|
|
159
161
|
def apply_drawing_call
|
160
|
-
if
|
161
|
-
|
162
|
+
return if state.disable_drawing || !drawable?
|
163
|
+
|
164
|
+
fill = computed_properties.fill != 'none'
|
165
|
+
stroke = computed_properties.stroke != 'none'
|
162
166
|
|
163
|
-
|
164
|
-
|
167
|
+
if fill
|
168
|
+
command = stroke ? 'fill_and_stroke' : 'fill'
|
169
|
+
|
170
|
+
if computed_properties.fill_rule == 'evenodd'
|
171
|
+
add_call_and_enter(command, {fill_rule: :even_odd})
|
165
172
|
else
|
166
|
-
add_call_and_enter(
|
173
|
+
add_call_and_enter(command)
|
167
174
|
end
|
175
|
+
elsif stroke
|
176
|
+
add_call_and_enter('stroke')
|
177
|
+
else
|
178
|
+
add_call_and_enter('end_path')
|
168
179
|
end
|
169
180
|
end
|
170
181
|
|
@@ -207,32 +218,20 @@ class Prawn::SVG::Elements::Base
|
|
207
218
|
end
|
208
219
|
|
209
220
|
def extract_attributes_and_properties
|
210
|
-
if
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
if classes = source.attributes["class"]
|
215
|
-
class_styles = classes.split(' ').collect do |class_name|
|
216
|
-
@document.css_parser.find_by_selector(".#{class_name}")
|
217
|
-
end
|
221
|
+
if styles = document.element_styles[source]
|
222
|
+
# TODO : implement !important, at the moment it's just ignored
|
223
|
+
styles.each do |name, value, _important|
|
224
|
+
@properties.set(name, value)
|
218
225
|
end
|
219
|
-
|
220
|
-
element_style = source.attributes['style']
|
221
|
-
|
222
|
-
style = [tag_style, class_styles, id_style, element_style].flatten.collect do |s|
|
223
|
-
s.nil? || s.strip == "" ? "" : "#{s}#{";" unless s.match(/;\s*\z/)}"
|
224
|
-
end.join
|
225
|
-
else
|
226
|
-
style = source.attributes['style'] || ""
|
227
226
|
end
|
228
227
|
|
228
|
+
@properties.load_hash(parse_css_declarations(source.attributes['style'] || ''))
|
229
|
+
|
229
230
|
source.attributes.each do |name, value|
|
230
231
|
# Properties#set returns nil if it's not a recognised property name
|
231
232
|
@properties.set(name, value) or @attributes[name] = value
|
232
233
|
end
|
233
234
|
|
234
|
-
@properties.load_hash(parse_css_declarations(style))
|
235
|
-
|
236
235
|
state.computed_properties.compute_properties(@properties)
|
237
236
|
end
|
238
237
|
|
@@ -268,4 +267,8 @@ class Prawn::SVG::Elements::Base
|
|
268
267
|
element = document.elements_by_id[matches[1]] if matches
|
269
268
|
element if element && (expected_type.nil? || element.name == expected_type)
|
270
269
|
end
|
270
|
+
|
271
|
+
def href_attribute
|
272
|
+
attributes['xlink:href'] || attributes['href']
|
273
|
+
end
|
271
274
|
end
|
@@ -1,9 +1,7 @@
|
|
1
1
|
class Prawn::SVG::Elements::Container < Prawn::SVG::Elements::Base
|
2
2
|
def parse
|
3
|
-
state.disable_drawing = true if name == 'clipPath'
|
4
|
-
|
5
3
|
set_display_none if name == 'symbol' && !state.inside_use
|
6
|
-
set_display_none if
|
4
|
+
set_display_none if name == 'defs'
|
7
5
|
end
|
8
6
|
|
9
7
|
def container?
|
@@ -1,10 +1,18 @@
|
|
1
1
|
class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
2
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
height = y1 - y2
|
37
|
+
private
|
26
38
|
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
32
|
-
to = [@x2, @y2]
|
44
|
+
width = bounding_x2 - bounding_x1
|
45
|
+
height = bounding_y1 - bounding_y2
|
33
46
|
end
|
34
47
|
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
106
|
-
|
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)
|
@@ -15,9 +15,9 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
|
|
15
15
|
|
16
16
|
raise SkipElementQuietly if state.computed_properties.display == "none"
|
17
17
|
|
18
|
-
@url =
|
18
|
+
@url = href_attribute
|
19
19
|
if @url.nil?
|
20
|
-
raise SkipElementError, "image tag must have an xlink:href"
|
20
|
+
raise SkipElementError, "image tag must have an href or xlink:href"
|
21
21
|
end
|
22
22
|
|
23
23
|
x = x(attributes['x'] || 0)
|
@@ -4,10 +4,31 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
4
4
|
|
5
5
|
INSIDE_SPACE_REGEXP = /[ \t\r\n,]*/
|
6
6
|
OUTSIDE_SPACE_REGEXP = /[ \t\r\n]*/
|
7
|
-
INSIDE_REGEXP = /#{INSIDE_SPACE_REGEXP}([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:(?<=[0-9])[eE][+-]?[0-9]+)?)/
|
8
|
-
|
7
|
+
INSIDE_REGEXP = /#{INSIDE_SPACE_REGEXP}(?>([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:(?<=[0-9])[eE][+-]?[0-9]+)?))/
|
8
|
+
FLAG_REGEXP = /#{INSIDE_SPACE_REGEXP}([01])/
|
9
9
|
COMMAND_REGEXP = /^#{OUTSIDE_SPACE_REGEXP}([A-Za-z])((?:#{INSIDE_REGEXP})*)#{OUTSIDE_SPACE_REGEXP}/
|
10
10
|
|
11
|
+
A_PARAMETERS_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{FLAG_REGEXP}#{FLAG_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}/
|
12
|
+
ONE_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}/
|
13
|
+
TWO_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}/
|
14
|
+
FOUR_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}/
|
15
|
+
SIX_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}/
|
16
|
+
|
17
|
+
COMMAND_MATCH_MAP = {
|
18
|
+
'A' => A_PARAMETERS_REGEXP,
|
19
|
+
'C' => SIX_PARAMETER_REGEXP,
|
20
|
+
'H' => ONE_PARAMETER_REGEXP,
|
21
|
+
'L' => TWO_PARAMETER_REGEXP,
|
22
|
+
'M' => TWO_PARAMETER_REGEXP,
|
23
|
+
'Q' => FOUR_PARAMETER_REGEXP,
|
24
|
+
'S' => FOUR_PARAMETER_REGEXP,
|
25
|
+
'T' => TWO_PARAMETER_REGEXP,
|
26
|
+
'V' => ONE_PARAMETER_REGEXP,
|
27
|
+
'Z' => //,
|
28
|
+
}
|
29
|
+
|
30
|
+
PARAMETERLESS_COMMANDS = COMMAND_MATCH_MAP.select { |_, v| v == // }.map(&:first)
|
31
|
+
|
11
32
|
FLOAT_ERROR_DELTA = 1e-10
|
12
33
|
|
13
34
|
attr_reader :commands
|
@@ -16,20 +37,20 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
16
37
|
require_attributes 'd'
|
17
38
|
|
18
39
|
@commands = []
|
40
|
+
@last_point = nil
|
19
41
|
|
20
42
|
data = attributes["d"].gsub(/#{OUTSIDE_SPACE_REGEXP}$/, '')
|
21
43
|
|
22
44
|
matched_commands = match_all(data, COMMAND_REGEXP)
|
23
45
|
raise SkipElementError, "Invalid/unsupported syntax for SVG path data" if matched_commands.nil?
|
24
46
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
47
|
+
matched_commands.each do |(command, parameters)|
|
48
|
+
regexp = COMMAND_MATCH_MAP[command.upcase] or break
|
49
|
+
matched_values = match_all(parameters, regexp) or break
|
50
|
+
values = matched_values.map { |value| value.map(&:to_f) }
|
51
|
+
break if values.empty? && !PARAMETERLESS_COMMANDS.include?(command.upcase)
|
52
|
+
|
53
|
+
parse_path_command(command, values)
|
33
54
|
end
|
34
55
|
end
|
35
56
|
|
@@ -48,8 +69,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
48
69
|
|
49
70
|
case upcase_command
|
50
71
|
when 'M' # moveto
|
51
|
-
x = values.shift
|
52
|
-
y = values.shift or throw :invalid_command
|
72
|
+
x, y = values.shift
|
53
73
|
|
54
74
|
if relative && @last_point
|
55
75
|
x += @last_point.first
|
@@ -68,8 +88,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
68
88
|
|
69
89
|
when 'L' # lineto
|
70
90
|
while values.any?
|
71
|
-
x = values.shift
|
72
|
-
y = values.shift or throw :invalid_command
|
91
|
+
x, y = values.shift
|
73
92
|
if relative && @last_point
|
74
93
|
x += @last_point.first
|
75
94
|
y += @last_point.last
|
@@ -80,22 +99,21 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
80
99
|
|
81
100
|
when 'H' # horizontal lineto
|
82
101
|
while values.any?
|
83
|
-
x = values.shift
|
102
|
+
x = values.shift.first
|
84
103
|
x += @last_point.first if relative && @last_point
|
85
104
|
push_command Prawn::SVG::Pathable::Line.new([x, @last_point.last])
|
86
105
|
end
|
87
106
|
|
88
107
|
when 'V' # vertical lineto
|
89
108
|
while values.any?
|
90
|
-
y = values.shift
|
109
|
+
y = values.shift.first
|
91
110
|
y += @last_point.last if relative && @last_point
|
92
111
|
push_command Prawn::SVG::Pathable::Line.new([@last_point.first, y])
|
93
112
|
end
|
94
113
|
|
95
114
|
when 'C' # curveto
|
96
115
|
while values.any?
|
97
|
-
x1, y1, x2, y2, x, y = values.shift
|
98
|
-
throw :invalid_command unless y
|
116
|
+
x1, y1, x2, y2, x, y = values.shift
|
99
117
|
|
100
118
|
if relative && @last_point
|
101
119
|
x += @last_point.first
|
@@ -112,8 +130,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
112
130
|
|
113
131
|
when 'S' # shorthand/smooth curveto
|
114
132
|
while values.any?
|
115
|
-
x2, y2, x, y = values.shift
|
116
|
-
throw :invalid_command unless y
|
133
|
+
x2, y2, x, y = values.shift
|
117
134
|
|
118
135
|
if relative && @last_point
|
119
136
|
x += @last_point.first
|
@@ -136,13 +153,11 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
136
153
|
when 'Q', 'T' # quadratic curveto
|
137
154
|
while values.any?
|
138
155
|
if shorthand = upcase_command == 'T'
|
139
|
-
x, y = values.shift
|
156
|
+
x, y = values.shift
|
140
157
|
else
|
141
|
-
x1, y1, x, y = values.shift
|
158
|
+
x1, y1, x, y = values.shift
|
142
159
|
end
|
143
160
|
|
144
|
-
throw :invalid_command unless y
|
145
|
-
|
146
161
|
if relative && @last_point
|
147
162
|
x += @last_point.first
|
148
163
|
x1 += @last_point.first if x1
|
@@ -174,8 +189,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
174
189
|
return unless @last_point
|
175
190
|
|
176
191
|
while values.any?
|
177
|
-
rx, ry, phi, fa, fs, x2, y2 = values.shift
|
178
|
-
throw :invalid_command unless y2
|
192
|
+
rx, ry, phi, fa, fs, x2, y2 = values.shift
|
179
193
|
|
180
194
|
x1, y1 = @last_point
|
181
195
|
|
@@ -276,9 +290,8 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
276
290
|
def match_all(string, regexp) # regexp must start with ^
|
277
291
|
result = []
|
278
292
|
while string != ""
|
279
|
-
matches = string.match(regexp)
|
280
|
-
result << matches
|
281
|
-
return if matches.nil?
|
293
|
+
matches = string.match(regexp) or return
|
294
|
+
result << matches.captures
|
282
295
|
string = matches.post_match
|
283
296
|
end
|
284
297
|
result
|