prawn-svg 0.27.1 → 0.31.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|