prawn-svg 0.34.2 → 0.35.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/lint.yml +19 -0
- data/.github/workflows/test.yml +14 -27
- data/.gitignore +0 -1
- data/.rubocop.yml +86 -0
- data/.rubocop_todo.yml +51 -0
- data/Gemfile +4 -3
- data/Gemfile.lock +81 -0
- data/README.md +1 -1
- data/Rakefile +1 -1
- data/lib/prawn/svg/attributes/clip_path.rb +3 -3
- data/lib/prawn/svg/attributes/opacity.rb +3 -3
- data/lib/prawn/svg/attributes/stroke.rb +9 -9
- data/lib/prawn/svg/attributes/transform.rb +2 -2
- data/lib/prawn/svg/attributes.rb +1 -1
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +17 -15
- data/lib/prawn/svg/calculators/aspect_ratio.rb +16 -14
- data/lib/prawn/svg/calculators/document_sizing.rb +9 -10
- data/lib/prawn/svg/calculators/pixels.rb +8 -5
- data/lib/prawn/svg/color.rb +209 -212
- data/lib/prawn/svg/css/font_family_parser.rb +2 -2
- data/lib/prawn/svg/css/selector_parser.rb +39 -35
- data/lib/prawn/svg/css/stylesheets.rb +24 -24
- data/lib/prawn/svg/css/values_parser.rb +68 -0
- data/lib/prawn/svg/document.rb +6 -5
- data/lib/prawn/svg/elements/base.rb +29 -34
- data/lib/prawn/svg/elements/circle.rb +4 -4
- data/lib/prawn/svg/elements/clip_path.rb +0 -1
- data/lib/prawn/svg/elements/depth_first_base.rb +6 -6
- data/lib/prawn/svg/elements/ellipse.rb +3 -4
- data/lib/prawn/svg/elements/gradient.rb +49 -51
- data/lib/prawn/svg/elements/image.rb +5 -5
- data/lib/prawn/svg/elements/marker.rb +5 -5
- data/lib/prawn/svg/elements/path.rb +46 -47
- data/lib/prawn/svg/elements/polygon.rb +1 -1
- data/lib/prawn/svg/elements/polyline.rb +2 -2
- data/lib/prawn/svg/elements/rect.rb +3 -3
- data/lib/prawn/svg/elements/text.rb +1 -1
- data/lib/prawn/svg/elements/text_component.rb +22 -22
- data/lib/prawn/svg/elements/use.rb +3 -7
- data/lib/prawn/svg/elements/viewport.rb +1 -3
- data/lib/prawn/svg/elements.rb +30 -29
- data/lib/prawn/svg/extension.rb +2 -1
- data/lib/prawn/svg/font.rb +7 -7
- data/lib/prawn/svg/font_registry.rb +13 -13
- data/lib/prawn/svg/gradients.rb +3 -2
- data/lib/prawn/svg/interface.rb +4 -3
- data/lib/prawn/svg/loaders/data.rb +2 -2
- data/lib/prawn/svg/loaders/file.rb +12 -14
- data/lib/prawn/svg/loaders/web.rb +4 -8
- data/lib/prawn/svg/pathable.rb +41 -37
- data/lib/prawn/svg/properties.rb +34 -33
- data/lib/prawn/svg/renderer.rb +7 -7
- data/lib/prawn/svg/state.rb +1 -1
- data/lib/prawn/svg/transform_parser.rb +5 -5
- data/lib/prawn/svg/ttf.rb +21 -17
- data/lib/prawn/svg/url_loader.rb +1 -1
- data/lib/prawn/svg/version.rb +1 -1
- data/lib/prawn-svg.rb +1 -0
- data/prawn-svg.gemspec +5 -7
- data/spec/integration_spec.rb +77 -70
- data/spec/prawn/svg/attributes/opacity_spec.rb +11 -15
- data/spec/prawn/svg/attributes/transform_spec.rb +6 -6
- data/spec/prawn/svg/calculators/aspect_ratio_spec.rb +50 -50
- data/spec/prawn/svg/calculators/document_sizing_spec.rb +35 -35
- data/spec/prawn/svg/calculators/pixels_spec.rb +31 -30
- data/spec/prawn/svg/color_spec.rb +31 -31
- data/spec/prawn/svg/css/font_family_parser_spec.rb +12 -12
- data/spec/prawn/svg/css/selector_parser_spec.rb +21 -21
- data/spec/prawn/svg/css/stylesheets_spec.rb +51 -45
- data/spec/prawn/svg/css/values_parser_spec.rb +16 -0
- data/spec/prawn/svg/document_spec.rb +15 -14
- data/spec/prawn/svg/elements/base_spec.rb +39 -34
- data/spec/prawn/svg/elements/gradient_spec.rb +39 -39
- data/spec/prawn/svg/elements/line_spec.rb +22 -22
- data/spec/prawn/svg/elements/marker_spec.rb +36 -40
- data/spec/prawn/svg/elements/path_spec.rb +134 -110
- data/spec/prawn/svg/elements/polygon_spec.rb +18 -18
- data/spec/prawn/svg/elements/polyline_spec.rb +16 -16
- data/spec/prawn/svg/elements/text_spec.rb +149 -127
- data/spec/prawn/svg/font_registry_spec.rb +34 -34
- data/spec/prawn/svg/font_spec.rb +4 -4
- data/spec/prawn/svg/interface_spec.rb +47 -39
- data/spec/prawn/svg/loaders/data_spec.rb +21 -21
- data/spec/prawn/svg/loaders/file_spec.rb +43 -40
- data/spec/prawn/svg/loaders/web_spec.rb +15 -15
- data/spec/prawn/svg/pathable_spec.rb +21 -21
- data/spec/prawn/svg/properties_spec.rb +51 -51
- data/spec/prawn/svg/transform_parser_spec.rb +12 -12
- data/spec/prawn/svg/ttf_spec.rb +5 -5
- data/spec/prawn/svg/url_loader_spec.rb +25 -23
- data/spec/spec_helper.rb +4 -4
- metadata +21 -146
@@ -3,15 +3,15 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
3
3
|
attr_reader :x1, :y1, :x2, :y2, :cx, :cy, :fx, :fy, :radius, :units, :stops, :transform_matrix
|
4
4
|
|
5
5
|
TAG_NAME_TO_TYPE = {
|
6
|
-
|
7
|
-
|
8
|
-
}
|
6
|
+
'linearGradient' => :linear,
|
7
|
+
'radialGradient' => :radial
|
8
|
+
}.freeze
|
9
9
|
|
10
10
|
def parse
|
11
11
|
# A gradient tag without an ID is inaccessible and can never be used
|
12
12
|
raise SkipElementQuietly if attributes['id'].nil?
|
13
13
|
|
14
|
-
@parent_gradient = document.gradients[href_attribute[1
|
14
|
+
@parent_gradient = document.gradients[href_attribute[1..]] if href_attribute && href_attribute[0] == '#'
|
15
15
|
|
16
16
|
assert_compatible_prawn_version
|
17
17
|
load_gradient_configuration
|
@@ -28,10 +28,10 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
28
28
|
# by a monkey patch installed by prawn-svg. Prawn only sees this as a truthy variable.
|
29
29
|
#
|
30
30
|
# See Prawn::SVG::Extensions::AdditionalGradientTransforms for details.
|
31
|
-
base_arguments = {stops: stops, apply_transformations: transform_matrix || true}
|
31
|
+
base_arguments = { stops: stops, apply_transformations: transform_matrix || true }
|
32
32
|
|
33
33
|
arguments = specific_gradient_arguments(element)
|
34
|
-
arguments
|
34
|
+
arguments&.merge(base_arguments)
|
35
35
|
end
|
36
36
|
|
37
37
|
private
|
@@ -47,31 +47,31 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
47
47
|
|
48
48
|
case [type, units]
|
49
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]
|
50
|
+
from = [bounding_x1 + (width * x1), bounding_y1 - (height * y1)]
|
51
|
+
to = [bounding_x1 + (width * x2), bounding_y1 - (height * y2)]
|
52
52
|
|
53
|
-
{from: from, to: to}
|
53
|
+
{ from: from, to: to }
|
54
54
|
|
55
55
|
when [:linear, :user_space]
|
56
|
-
{from: [x1, y1], to: [x2, y2]}
|
56
|
+
{ from: [x1, y1], to: [x2, y2] }
|
57
57
|
|
58
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]
|
59
|
+
center = [bounding_x1 + (width * cx), bounding_y1 - (height * cy)]
|
60
|
+
focus = [bounding_x1 + (width * fx), bounding_y1 - (height * fy)]
|
61
61
|
|
62
|
-
#
|
62
|
+
# NOTE: Chrome, at least, implements radial bounding box radiuses as
|
63
63
|
# having separate X and Y components, so in bounding box mode their
|
64
64
|
# gradients come out as ovals instead of circles. PDF radial shading
|
65
65
|
# doesn't have the option to do this, and it's confusing why the
|
66
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}
|
67
|
+
hypot = Math.sqrt((width * width) + (height * height))
|
68
|
+
{ from: focus, r1: 0, to: center, r2: radius * hypot }
|
69
69
|
|
70
70
|
when [:radial, :user_space]
|
71
|
-
{from: [fx, fy], r1: 0, to: [cx, cy], r2: radius}
|
71
|
+
{ from: [fx, fy], r1: 0, to: [cx, cy], r2: radius }
|
72
72
|
|
73
73
|
else
|
74
|
-
raise
|
74
|
+
raise 'unexpected type/unit system'
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
@@ -80,19 +80,19 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def assert_compatible_prawn_version
|
83
|
-
if (Prawn::VERSION.split(
|
83
|
+
if (Prawn::VERSION.split('.').map(&:to_i) <=> [2, 2, 0]) == -1
|
84
84
|
raise SkipElementError, "Prawn 2.2.0+ must be used if you'd like prawn-svg to render gradients"
|
85
85
|
end
|
86
86
|
end
|
87
87
|
|
88
88
|
def load_gradient_configuration
|
89
|
-
@units = attributes[
|
89
|
+
@units = attributes['gradientUnits'] == 'userSpaceOnUse' ? :user_space : :bounding_box
|
90
90
|
|
91
|
-
if transform = attributes[
|
91
|
+
if (transform = attributes['gradientTransform'])
|
92
92
|
@transform_matrix = parse_transform_attribute(transform)
|
93
93
|
end
|
94
94
|
|
95
|
-
if (spread_method = attributes['spreadMethod']) && spread_method !=
|
95
|
+
if (spread_method = attributes['spreadMethod']) && spread_method != 'pad'
|
96
96
|
warnings << "prawn-svg only currently supports the 'pad' spreadMethod attribute value"
|
97
97
|
end
|
98
98
|
end
|
@@ -100,33 +100,33 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
100
100
|
def load_coordinates
|
101
101
|
case [type, units]
|
102
102
|
when [:linear, :bounding_box]
|
103
|
-
@x1 = parse_zero_to_one(attributes[
|
104
|
-
@y1 = parse_zero_to_one(attributes[
|
105
|
-
@x2 = parse_zero_to_one(attributes[
|
106
|
-
@y2 = parse_zero_to_one(attributes[
|
103
|
+
@x1 = parse_zero_to_one(attributes['x1'], 0)
|
104
|
+
@y1 = parse_zero_to_one(attributes['y1'], 0)
|
105
|
+
@x2 = parse_zero_to_one(attributes['x2'], 1)
|
106
|
+
@y2 = parse_zero_to_one(attributes['y2'], 0)
|
107
107
|
|
108
108
|
when [:linear, :user_space]
|
109
|
-
@x1 = x(attributes[
|
110
|
-
@y1 = y(attributes[
|
111
|
-
@x2 = x(attributes[
|
112
|
-
@y2 = y(attributes[
|
109
|
+
@x1 = x(attributes['x1'])
|
110
|
+
@y1 = y(attributes['y1'])
|
111
|
+
@x2 = x(attributes['x2'])
|
112
|
+
@y2 = y(attributes['y2'])
|
113
113
|
|
114
114
|
when [:radial, :bounding_box]
|
115
|
-
@cx = parse_zero_to_one(attributes[
|
116
|
-
@cy = parse_zero_to_one(attributes[
|
117
|
-
@fx = parse_zero_to_one(attributes[
|
118
|
-
@fy = parse_zero_to_one(attributes[
|
119
|
-
@radius = parse_zero_to_one(attributes[
|
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
120
|
|
121
121
|
when [:radial, :user_space]
|
122
|
-
@cx = x(attributes[
|
123
|
-
@cy = y(attributes[
|
124
|
-
@fx = x(attributes[
|
125
|
-
@fy = y(attributes[
|
126
|
-
@radius = pixels(attributes[
|
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
127
|
|
128
128
|
else
|
129
|
-
raise
|
129
|
+
raise 'unexpected type/unit system'
|
130
130
|
end
|
131
131
|
end
|
132
132
|
|
@@ -136,40 +136,38 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
136
136
|
element.process
|
137
137
|
element
|
138
138
|
end.select do |element|
|
139
|
-
element.name == 'stop' && element.attributes[
|
139
|
+
element.name == 'stop' && element.attributes['offset']
|
140
140
|
end
|
141
141
|
|
142
142
|
@stops = stop_elements.each.with_object([]) do |child, result|
|
143
|
-
offset = parse_zero_to_one(child.attributes[
|
143
|
+
offset = parse_zero_to_one(child.attributes['offset'])
|
144
144
|
|
145
145
|
# Offsets must be strictly increasing (SVG 13.2.4)
|
146
|
-
if result.last && result.last.first > offset
|
147
|
-
offset = result.last.first
|
148
|
-
end
|
146
|
+
offset = result.last.first if result.last && result.last.first > offset
|
149
147
|
|
150
|
-
if color = Prawn::SVG::Color.css_color_to_prawn_color(child.properties.stop_color)
|
148
|
+
if (color = Prawn::SVG::Color.css_color_to_prawn_color(child.properties.stop_color))
|
151
149
|
result << [offset, color]
|
152
150
|
end
|
153
151
|
end
|
154
152
|
|
155
153
|
if stops.empty?
|
156
154
|
if parent_gradient.nil? || parent_gradient.stops.empty?
|
157
|
-
raise SkipElementError,
|
155
|
+
raise SkipElementError, 'gradient does not have any valid stops'
|
158
156
|
end
|
159
157
|
|
160
158
|
@stops = parent_gradient.stops
|
161
159
|
else
|
162
|
-
stops.unshift([0, stops.first.last]) if stops.first.first
|
163
|
-
stops.push([1, stops.last.last]) if stops.last.first
|
160
|
+
stops.unshift([0, stops.first.last]) if stops.first.first.positive?
|
161
|
+
stops.push([1, stops.last.last]) if stops.last.first < 1
|
164
162
|
end
|
165
163
|
end
|
166
164
|
|
167
165
|
def parse_zero_to_one(string, default = 0)
|
168
166
|
string = string.to_s.strip
|
169
|
-
return default if string ==
|
167
|
+
return default if string == ''
|
170
168
|
|
171
169
|
value = string.to_f
|
172
|
-
value /= 100.0 if string[-1
|
170
|
+
value /= 100.0 if string[-1..] == '%'
|
173
171
|
[0.0, value, 1.0].sort[1]
|
174
172
|
end
|
175
173
|
end
|
@@ -25,7 +25,7 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
|
|
25
25
|
y = y(attributes['y'] || 0)
|
26
26
|
width = x_pixels(attributes['width'])
|
27
27
|
height = y_pixels(attributes['height'])
|
28
|
-
|
28
|
+
preserve_aspect_ratio = attributes['preserveAspectRatio']
|
29
29
|
|
30
30
|
raise SkipElementQuietly if width.zero? || height.zero?
|
31
31
|
|
@@ -37,9 +37,9 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
|
|
37
37
|
raise SkipElementError, "Error retrieving URL #{@url}: #{e.message}"
|
38
38
|
end
|
39
39
|
|
40
|
-
@image_data = process_image(@image, width, height,
|
40
|
+
@image_data = process_image(@image, width, height, preserve_aspect_ratio)
|
41
41
|
|
42
|
-
@aspect = Prawn::SVG::Calculators::AspectRatio.new(
|
42
|
+
@aspect = Prawn::SVG::Calculators::AspectRatio.new(preserve_aspect_ratio, [width, height], @image_data.dimensions)
|
43
43
|
|
44
44
|
@clip_x = x
|
45
45
|
@clip_y = y
|
@@ -77,7 +77,7 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
|
|
77
77
|
|
78
78
|
protected
|
79
79
|
|
80
|
-
def process_image(data, width, height,
|
80
|
+
def process_image(data, width, height, preserve_aspect_ratio)
|
81
81
|
if (handler = find_image_handler(data))
|
82
82
|
image = handler.new(data)
|
83
83
|
ImageData.new([image.width.to_f, image.height.to_f], nil)
|
@@ -85,7 +85,7 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
|
|
85
85
|
elsif potentially_svg?(data)
|
86
86
|
document = Prawn::SVG::Document.new(
|
87
87
|
data, [width, height], { width: width, height: height },
|
88
|
-
attribute_overrides: { 'preserveAspectRatio' =>
|
88
|
+
attribute_overrides: { 'preserveAspectRatio' => preserve_aspect_ratio }
|
89
89
|
)
|
90
90
|
|
91
91
|
dimensions = [document.sizing.output_width, document.sizing.output_height]
|
@@ -10,12 +10,12 @@ class Prawn::SVG::Elements::Marker < Prawn::SVG::Elements::Base
|
|
10
10
|
|
11
11
|
def apply_marker(element, point: nil, angle: 0)
|
12
12
|
sizing = Prawn::SVG::Calculators::DocumentSizing.new([0, 0], attributes)
|
13
|
-
sizing.document_width = attributes[
|
14
|
-
sizing.document_height = attributes[
|
13
|
+
sizing.document_width = attributes['markerWidth'] || 3
|
14
|
+
sizing.document_height = attributes['markerHeight'] || 3
|
15
15
|
sizing.calculate
|
16
16
|
|
17
17
|
if sizing.invalid?
|
18
|
-
document.warnings <<
|
18
|
+
document.warnings << '<marker> cannot be rendered due to invalid sizing information'
|
19
19
|
return
|
20
20
|
end
|
21
21
|
|
@@ -48,8 +48,8 @@ class Prawn::SVG::Elements::Marker < Prawn::SVG::Elements::Base
|
|
48
48
|
|
49
49
|
if overflow_hidden?
|
50
50
|
point = [sizing.x_offset * sizing.x_scale, y(sizing.y_offset * sizing.y_scale)]
|
51
|
-
element.add_call
|
52
|
-
element.add_call
|
51
|
+
element.add_call 'rectangle', point, sizing.output_width, sizing.output_height
|
52
|
+
element.add_call 'clip'
|
53
53
|
end
|
54
54
|
|
55
55
|
element.add_call 'transformation_matrix', sizing.x_scale, 0, 0, sizing.y_scale, 0, 0
|
@@ -2,17 +2,17 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
2
2
|
include Prawn::SVG::Calculators::ArcToBezierCurve
|
3
3
|
include Prawn::SVG::Pathable
|
4
4
|
|
5
|
-
INSIDE_SPACE_REGEXP = /[ \t\r\n,]
|
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
|
-
FLAG_REGEXP = /#{INSIDE_SPACE_REGEXP}([01])
|
9
|
-
COMMAND_REGEXP = /^#{OUTSIDE_SPACE_REGEXP}([A-Za-z])((?:#{INSIDE_REGEXP})*)#{OUTSIDE_SPACE_REGEXP}
|
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}
|
5
|
+
INSIDE_SPACE_REGEXP = /[ \t\r\n,]*/.freeze
|
6
|
+
OUTSIDE_SPACE_REGEXP = /[ \t\r\n]*/.freeze
|
7
|
+
INSIDE_REGEXP = /#{INSIDE_SPACE_REGEXP}(?>([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:(?<=[0-9])[eE][+-]?[0-9]+)?))/.freeze
|
8
|
+
FLAG_REGEXP = /#{INSIDE_SPACE_REGEXP}([01])/.freeze
|
9
|
+
COMMAND_REGEXP = /^#{OUTSIDE_SPACE_REGEXP}([A-Za-z])((?:#{INSIDE_REGEXP})*)#{OUTSIDE_SPACE_REGEXP}/.freeze
|
10
|
+
|
11
|
+
A_PARAMETERS_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{FLAG_REGEXP}#{FLAG_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}/.freeze
|
12
|
+
ONE_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}/.freeze
|
13
|
+
TWO_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}/.freeze
|
14
|
+
FOUR_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}/.freeze
|
15
|
+
SIX_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}/.freeze
|
16
16
|
|
17
17
|
COMMAND_MATCH_MAP = {
|
18
18
|
'A' => A_PARAMETERS_REGEXP,
|
@@ -24,8 +24,8 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
24
24
|
'S' => FOUR_PARAMETER_REGEXP,
|
25
25
|
'T' => TWO_PARAMETER_REGEXP,
|
26
26
|
'V' => ONE_PARAMETER_REGEXP,
|
27
|
-
'Z' =>
|
28
|
-
}
|
27
|
+
'Z' => //
|
28
|
+
}.freeze
|
29
29
|
|
30
30
|
PARAMETERLESS_COMMANDS = COMMAND_MATCH_MAP.select { |_, v| v == // }.map(&:first)
|
31
31
|
|
@@ -39,10 +39,10 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
39
39
|
@commands = []
|
40
40
|
@last_point = nil
|
41
41
|
|
42
|
-
data = attributes[
|
42
|
+
data = attributes['d'].gsub(/#{OUTSIDE_SPACE_REGEXP}$/, '')
|
43
43
|
|
44
44
|
matched_commands = match_all(data, COMMAND_REGEXP)
|
45
|
-
raise SkipElementError,
|
45
|
+
raise SkipElementError, 'Invalid/unsupported syntax for SVG path data' if matched_commands.nil?
|
46
46
|
|
47
47
|
matched_commands.each do |(command, parameters)|
|
48
48
|
regexp = COMMAND_MATCH_MAP[command.upcase] or break
|
@@ -80,9 +80,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
80
80
|
return parse_path_command(relative ? 'l' : 'L', values) if values.any?
|
81
81
|
|
82
82
|
when 'Z' # closepath
|
83
|
-
if @subpath_initial_point
|
84
|
-
push_command Prawn::SVG::Pathable::Close.new(@subpath_initial_point)
|
85
|
-
end
|
83
|
+
push_command Prawn::SVG::Pathable::Close.new(@subpath_initial_point) if @subpath_initial_point
|
86
84
|
|
87
85
|
when 'L' # lineto
|
88
86
|
while values.any?
|
@@ -138,8 +136,8 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
138
136
|
end
|
139
137
|
|
140
138
|
if @previous_control_point
|
141
|
-
x1 = 2 * @last_point.first - @previous_control_point.first
|
142
|
-
y1 = 2 * @last_point.last - @previous_control_point.last
|
139
|
+
x1 = (2 * @last_point.first) - @previous_control_point.first
|
140
|
+
y1 = (2 * @last_point.last) - @previous_control_point.last
|
143
141
|
else
|
144
142
|
x1, y1 = @last_point
|
145
143
|
end
|
@@ -150,7 +148,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
150
148
|
|
151
149
|
when 'Q', 'T' # quadratic curveto
|
152
150
|
while values.any?
|
153
|
-
if shorthand = upcase_command == 'T'
|
151
|
+
if (shorthand = upcase_command == 'T')
|
154
152
|
x, y = values.shift
|
155
153
|
else
|
156
154
|
x1, y1, x, y = values.shift
|
@@ -165,18 +163,18 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
165
163
|
|
166
164
|
if shorthand
|
167
165
|
if @previous_quadratic_control_point
|
168
|
-
x1 = 2 * @last_point.first - @previous_quadratic_control_point.first
|
169
|
-
y1 = 2 * @last_point.last - @previous_quadratic_control_point.last
|
166
|
+
x1 = (2 * @last_point.first) - @previous_quadratic_control_point.first
|
167
|
+
y1 = (2 * @last_point.last) - @previous_quadratic_control_point.last
|
170
168
|
else
|
171
169
|
x1, y1 = @last_point
|
172
170
|
end
|
173
171
|
end
|
174
172
|
|
175
173
|
# convert from quadratic to cubic
|
176
|
-
cx1 = @last_point.first + (x1 - @last_point.first) * 2 / 3.0
|
177
|
-
cy1 = @last_point.last + (y1 - @last_point.last) * 2 / 3.0
|
178
|
-
cx2 = cx1 + (x - @last_point.first) / 3.0
|
179
|
-
cy2 = cy1 + (y - @last_point.last) / 3.0
|
174
|
+
cx1 = @last_point.first + ((x1 - @last_point.first) * 2 / 3.0)
|
175
|
+
cy1 = @last_point.last + ((y1 - @last_point.last) * 2 / 3.0)
|
176
|
+
cx2 = cx1 + ((x - @last_point.first) / 3.0)
|
177
|
+
cy2 = cy1 + ((y - @last_point.last) / 3.0)
|
180
178
|
|
181
179
|
@previous_quadratic_control_point = [x1, y1]
|
182
180
|
|
@@ -216,13 +214,13 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
216
214
|
# points. To do this, we use the algorithm documented in the SVG specification section F.6.5.
|
217
215
|
|
218
216
|
# F.6.5.1
|
219
|
-
xp1 = Math.cos(phi) * ((x1-x2)/2.0) + Math.sin(phi) * ((y1-y2)/2.0)
|
220
|
-
yp1 = -Math.sin(phi) * ((x1-x2)/2.0) + Math.cos(phi) * ((y1-y2)/2.0)
|
217
|
+
xp1 = (Math.cos(phi) * ((x1 - x2) / 2.0)) + (Math.sin(phi) * ((y1 - y2) / 2.0))
|
218
|
+
yp1 = (-Math.sin(phi) * ((x1 - x2) / 2.0)) + (Math.cos(phi) * ((y1 - y2) / 2.0))
|
221
219
|
|
222
220
|
# F.6.6.2
|
223
221
|
r2x = rx * rx
|
224
222
|
r2y = ry * ry
|
225
|
-
hat = xp1 * xp1 / r2x + yp1 * yp1 / r2y
|
223
|
+
hat = (xp1 * xp1 / r2x) + (yp1 * yp1 / r2y)
|
226
224
|
if hat > 1
|
227
225
|
rx *= Math.sqrt(hat)
|
228
226
|
ry *= Math.sqrt(hat)
|
@@ -231,22 +229,22 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
231
229
|
# F.6.5.2
|
232
230
|
r2x = rx * rx
|
233
231
|
r2y = ry * ry
|
234
|
-
square = (r2x * r2y - r2x * yp1 * yp1 - r2y * xp1 * xp1) / (r2x * yp1 * yp1 + r2y * xp1 * xp1)
|
235
|
-
square = 0 if square
|
232
|
+
square = ((r2x * r2y) - (r2x * yp1 * yp1) - (r2y * xp1 * xp1)) / ((r2x * yp1 * yp1) + (r2y * xp1 * xp1))
|
233
|
+
square = 0 if square.negative? && square > -FLOAT_ERROR_DELTA # catch rounding errors
|
236
234
|
base = Math.sqrt(square)
|
237
235
|
base *= -1 if fa == fs
|
238
236
|
cpx = base * rx * yp1 / ry
|
239
237
|
cpy = base * -ry * xp1 / rx
|
240
238
|
|
241
239
|
# F.6.5.3
|
242
|
-
cx = Math.cos(phi) * cpx + -Math.sin(phi) * cpy + (x1 + x2) / 2
|
243
|
-
cy = Math.sin(phi) * cpx + Math.cos(phi) * cpy + (y1 + y2) / 2
|
240
|
+
cx = (Math.cos(phi) * cpx) + (-Math.sin(phi) * cpy) + ((x1 + x2) / 2)
|
241
|
+
cy = (Math.sin(phi) * cpx) + (Math.cos(phi) * cpy) + ((y1 + y2) / 2)
|
244
242
|
|
245
243
|
# F.6.5.5
|
246
244
|
vx = (xp1 - cpx) / rx
|
247
245
|
vy = (yp1 - cpy) / ry
|
248
|
-
theta_1 = Math.acos(vx / Math.sqrt(vx * vx + vy * vy))
|
249
|
-
theta_1 *= -1 if vy
|
246
|
+
theta_1 = Math.acos(vx / Math.sqrt((vx * vx) + (vy * vy)))
|
247
|
+
theta_1 *= -1 if vy.negative?
|
250
248
|
|
251
249
|
# F.6.5.6
|
252
250
|
ux = vx
|
@@ -254,19 +252,19 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
254
252
|
vx = (-xp1 - cpx) / rx
|
255
253
|
vy = (-yp1 - cpy) / ry
|
256
254
|
|
257
|
-
numerator = ux * vx + uy * vy
|
258
|
-
denominator = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy)
|
255
|
+
numerator = (ux * vx) + (uy * vy)
|
256
|
+
denominator = Math.sqrt((ux * ux) + (uy * uy)) * Math.sqrt((vx * vx) + (vy * vy))
|
259
257
|
division = numerator / denominator
|
260
258
|
division = -1 if division < -1 # for rounding errors
|
261
259
|
|
262
260
|
d_theta = Math.acos(division) % (2 * Math::PI)
|
263
|
-
d_theta *= -1 if ux * vy - uy * vx
|
261
|
+
d_theta *= -1 if ((ux * vy) - (uy * vx)).negative?
|
264
262
|
|
265
263
|
# Adjust range
|
266
|
-
if fs
|
267
|
-
d_theta -= 2 * Math::PI if d_theta
|
268
|
-
|
269
|
-
d_theta += 2 * Math::PI
|
264
|
+
if fs.zero?
|
265
|
+
d_theta -= 2 * Math::PI if d_theta.positive?
|
266
|
+
elsif d_theta.negative?
|
267
|
+
d_theta += 2 * Math::PI
|
270
268
|
end
|
271
269
|
|
272
270
|
theta_2 = theta_1 + d_theta
|
@@ -277,17 +275,18 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
|
|
277
275
|
end
|
278
276
|
end
|
279
277
|
|
280
|
-
@previous_control_point = nil unless %w
|
281
|
-
@previous_quadratic_control_point = nil unless %w
|
278
|
+
@previous_control_point = nil unless %w[C S].include?(upcase_command)
|
279
|
+
@previous_quadratic_control_point = nil unless %w[Q T].include?(upcase_command)
|
282
280
|
end
|
283
281
|
|
284
282
|
def within_float_delta?(a, b)
|
285
283
|
(a - b).abs < FLOAT_ERROR_DELTA
|
286
284
|
end
|
287
285
|
|
288
|
-
|
286
|
+
# regexp must start with ^
|
287
|
+
def match_all(string, regexp)
|
289
288
|
result = []
|
290
|
-
while string !=
|
289
|
+
while string != ''
|
291
290
|
matches = string.match(regexp) or return
|
292
291
|
result << matches.captures
|
293
292
|
string = matches.post_match
|
@@ -16,7 +16,7 @@ class Prawn::SVG::Elements::Polygon < Prawn::SVG::Elements::Base
|
|
16
16
|
def commands
|
17
17
|
@commands ||= [
|
18
18
|
Prawn::SVG::Pathable::Move.new(@points[0])
|
19
|
-
] + @points[1
|
19
|
+
] + @points[1..].map { |point|
|
20
20
|
Prawn::SVG::Pathable::Line.new(point)
|
21
21
|
} + [
|
22
22
|
Prawn::SVG::Pathable::Close.new(@points[0])
|
@@ -16,8 +16,8 @@ class Prawn::SVG::Elements::Polyline < Prawn::SVG::Elements::Base
|
|
16
16
|
def commands
|
17
17
|
@commands ||= [
|
18
18
|
Prawn::SVG::Pathable::Move.new(@points[0])
|
19
|
-
] + @points[1
|
19
|
+
] + @points[1..].map do |point|
|
20
20
|
Prawn::SVG::Pathable::Line.new(point)
|
21
|
-
|
21
|
+
end
|
22
22
|
end
|
23
23
|
end
|
@@ -14,16 +14,16 @@ class Prawn::SVG::Elements::Rect < Prawn::SVG::Elements::Base
|
|
14
14
|
# If you implement separate rx and ry in the future, you'll want to change this
|
15
15
|
# so that rx is constrained to @width/2 and ry is constrained to @height/2.
|
16
16
|
max_value = [@width, @height].min / 2.0
|
17
|
-
@radius = clamp(
|
17
|
+
@radius = @radius.clamp(0, max_value)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
21
|
def apply
|
22
22
|
if @radius
|
23
23
|
# n.b. does not support both rx and ry being specified with different values
|
24
|
-
add_call
|
24
|
+
add_call 'rounded_rectangle', [@x, @y], @width, @height, @radius
|
25
25
|
else
|
26
|
-
add_call
|
26
|
+
add_call 'rectangle', [@x, @y], @width, @height
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
@@ -5,19 +5,17 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
|
|
5
5
|
TextState = Struct.new(:parent, :x, :y, :dx, :dy, :rotation, :spacing, :mode, :text_length, :length_adjust)
|
6
6
|
|
7
7
|
def parse
|
8
|
-
if state.inside_clip_path
|
9
|
-
raise SkipElementError, "<text> elements are not supported in clip paths"
|
10
|
-
end
|
8
|
+
raise SkipElementError, '<text> elements are not supported in clip paths' if state.inside_clip_path
|
11
9
|
|
12
10
|
if state.text.nil?
|
13
|
-
raise SkipElementError,
|
11
|
+
raise SkipElementError, 'attempted to <use> an component inside a text element, this is not supported'
|
14
12
|
end
|
15
13
|
|
16
|
-
state.text.x = (attributes['x'] ||
|
17
|
-
state.text.y = (attributes['y'] ||
|
18
|
-
state.text.dx = (attributes['dx'] ||
|
19
|
-
state.text.dy = (attributes['dy'] ||
|
20
|
-
state.text.rotation = (attributes['rotate'] ||
|
14
|
+
state.text.x = (attributes['x'] || '').split(COMMA_WSP_REGEXP).collect { |n| x(n) }
|
15
|
+
state.text.y = (attributes['y'] || '').split(COMMA_WSP_REGEXP).collect { |n| y(n) }
|
16
|
+
state.text.dx = (attributes['dx'] || '').split(COMMA_WSP_REGEXP).collect { |n| x_pixels(n) }
|
17
|
+
state.text.dy = (attributes['dy'] || '').split(COMMA_WSP_REGEXP).collect { |n| y_pixels(n) }
|
18
|
+
state.text.rotation = (attributes['rotate'] || '').split(COMMA_WSP_REGEXP).collect(&:to_f)
|
21
19
|
state.text.text_length = normalize_length(attributes['textLength'])
|
22
20
|
state.text.length_adjust = attributes['lengthAdjust']
|
23
21
|
state.text.spacing = calculate_character_spacing
|
@@ -40,7 +38,7 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
|
|
40
38
|
end
|
41
39
|
|
42
40
|
def apply
|
43
|
-
raise SkipElementQuietly if computed_properties.display ==
|
41
|
+
raise SkipElementQuietly if computed_properties.display == 'none'
|
44
42
|
|
45
43
|
font = select_font
|
46
44
|
apply_font(font) if font
|
@@ -49,18 +47,21 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
|
|
49
47
|
# and so we handle them in Prawn::SVG::Interface#rewrite_call_arguments
|
50
48
|
opts = {
|
51
49
|
size: computed_properties.numerical_font_size,
|
52
|
-
style: font
|
53
|
-
text_anchor: computed_properties.text_anchor
|
50
|
+
style: font&.subfamily,
|
51
|
+
text_anchor: computed_properties.text_anchor
|
54
52
|
}
|
55
53
|
|
56
|
-
|
54
|
+
unless computed_properties.dominant_baseline == 'auto'
|
55
|
+
opts[:dominant_baseline] =
|
56
|
+
computed_properties.dominant_baseline
|
57
|
+
end
|
57
58
|
opts[:decoration] = computed_properties.text_decoration unless computed_properties.text_decoration == 'none'
|
58
59
|
|
59
60
|
if state.text.parent
|
60
61
|
add_call_and_enter 'character_spacing', state.text.spacing unless state.text.spacing == state.text.parent.spacing
|
61
62
|
add_call_and_enter 'text_rendering_mode', state.text.mode unless state.text.mode == state.text.parent.mode
|
62
63
|
else
|
63
|
-
add_call_and_enter 'character_spacing', state.text.spacing unless state.text.spacing
|
64
|
+
add_call_and_enter 'character_spacing', state.text.spacing unless state.text.spacing.zero?
|
64
65
|
add_call_and_enter 'text_rendering_mode', state.text.mode unless state.text.mode == :fill
|
65
66
|
end
|
66
67
|
|
@@ -107,7 +108,7 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
|
|
107
108
|
end
|
108
109
|
|
109
110
|
def apply_text(text, opts)
|
110
|
-
while text !=
|
111
|
+
while text != ''
|
111
112
|
x = y = dx = dy = rotate = nil
|
112
113
|
remaining = rotation_remaining = false
|
113
114
|
|
@@ -152,7 +153,7 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
|
|
152
153
|
|
153
154
|
if remaining
|
154
155
|
add_call 'draw_text', text[0..0], **opts.dup
|
155
|
-
text = text[1
|
156
|
+
text = text[1..]
|
156
157
|
else
|
157
158
|
add_call 'draw_text', text, **opts.dup
|
158
159
|
|
@@ -160,11 +161,11 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
|
|
160
161
|
# solve this by shifting them out by the number of
|
161
162
|
# characters we've just drawn
|
162
163
|
shift = text.length - 1
|
163
|
-
if rotation_remaining && shift
|
164
|
+
if rotation_remaining && shift.positive?
|
164
165
|
list = state.text
|
165
166
|
while list
|
166
167
|
count = [shift, list.rotation.length - 1].min
|
167
|
-
list.rotation.shift(count) if count
|
168
|
+
list.rotation.shift(count) if count.positive?
|
168
169
|
list = list.parent
|
169
170
|
end
|
170
171
|
end
|
@@ -197,7 +198,7 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
|
|
197
198
|
href = href_attribute
|
198
199
|
|
199
200
|
if href && href[0..0] == '#'
|
200
|
-
element = document.elements_by_id[href[1
|
201
|
+
element = document.elements_by_id[href[1..]]
|
201
202
|
element if element.name == 'text'
|
202
203
|
end
|
203
204
|
end
|
@@ -241,10 +242,9 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
|
|
241
242
|
end
|
242
243
|
|
243
244
|
# overridden, we don't want to call fill/stroke as draw_text does this for us
|
244
|
-
def apply_drawing_call
|
245
|
-
end
|
245
|
+
def apply_drawing_call; end
|
246
246
|
|
247
247
|
def normalize_length(length)
|
248
|
-
x_pixels(length) if length
|
248
|
+
x_pixels(length) if length&.match(/\d/)
|
249
249
|
end
|
250
250
|
end
|