prawn-svg 0.21.0 → 0.22.1
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 +4 -4
- data/.travis.yml +6 -0
- data/README.md +11 -4
- data/lib/prawn-svg.rb +9 -6
- data/lib/prawn/svg/attributes.rb +6 -0
- data/lib/prawn/svg/attributes/clip_path.rb +17 -0
- data/lib/prawn/svg/attributes/display.rb +5 -0
- data/lib/prawn/svg/attributes/font.rb +38 -0
- data/lib/prawn/svg/attributes/opacity.rb +15 -0
- data/lib/prawn/svg/attributes/stroke.rb +35 -0
- data/lib/prawn/svg/attributes/transform.rb +50 -0
- data/lib/prawn/svg/calculators/aspect_ratio.rb +1 -1
- data/lib/prawn/svg/calculators/document_sizing.rb +2 -2
- data/lib/prawn/svg/calculators/pixels.rb +1 -1
- data/lib/prawn/svg/color.rb +44 -14
- data/lib/prawn/svg/document.rb +6 -5
- data/lib/prawn/svg/elements.rb +33 -0
- data/lib/prawn/svg/elements/base.rb +228 -0
- data/lib/prawn/svg/elements/circle.rb +25 -0
- data/lib/prawn/svg/elements/container.rb +15 -0
- data/lib/prawn/svg/elements/ellipse.rb +23 -0
- data/lib/prawn/svg/elements/gradient.rb +117 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +85 -0
- data/lib/prawn/svg/elements/line.rb +16 -0
- data/lib/prawn/svg/elements/path.rb +405 -0
- data/lib/prawn/svg/elements/polygon.rb +17 -0
- data/lib/prawn/svg/elements/polyline.rb +22 -0
- data/lib/prawn/svg/elements/rect.rb +33 -0
- data/lib/prawn/svg/elements/root.rb +9 -0
- data/lib/prawn/svg/elements/style.rb +10 -0
- data/lib/prawn/svg/elements/text.rb +87 -0
- data/lib/prawn/svg/elements/use.rb +29 -0
- data/lib/prawn/svg/extension.rb +2 -2
- data/lib/prawn/svg/font.rb +3 -3
- data/lib/prawn/svg/interface.rb +12 -5
- data/lib/prawn/svg/url_loader.rb +1 -1
- data/lib/prawn/svg/version.rb +2 -2
- data/prawn-svg.gemspec +3 -3
- data/spec/integration_spec.rb +59 -2
- data/spec/prawn/svg/attributes/font_spec.rb +49 -0
- data/spec/prawn/svg/attributes/transform_spec.rb +56 -0
- data/spec/prawn/svg/calculators/aspect_ratio_spec.rb +2 -2
- data/spec/prawn/svg/calculators/document_sizing_spec.rb +3 -3
- data/spec/prawn/svg/color_spec.rb +36 -15
- data/spec/prawn/svg/document_spec.rb +4 -4
- data/spec/prawn/svg/elements/base_spec.rb +125 -0
- data/spec/prawn/svg/elements/gradient_spec.rb +61 -0
- data/spec/prawn/svg/elements/path_spec.rb +123 -0
- data/spec/prawn/svg/elements/style_spec.rb +23 -0
- data/spec/prawn/svg/{parser → elements}/text_spec.rb +7 -8
- data/spec/prawn/svg/font_spec.rb +12 -12
- data/spec/prawn/svg/interface_spec.rb +7 -7
- data/spec/prawn/svg/url_loader_spec.rb +2 -2
- data/spec/sample_svg/gradients.svg +40 -0
- data/spec/sample_svg/rect02.svg +8 -11
- data/spec/spec_helper.rb +1 -1
- metadata +46 -18
- data/lib/prawn/svg/element.rb +0 -304
- data/lib/prawn/svg/parser.rb +0 -268
- data/lib/prawn/svg/parser/image.rb +0 -81
- data/lib/prawn/svg/parser/path.rb +0 -392
- data/lib/prawn/svg/parser/text.rb +0 -80
- data/spec/prawn/svg/element_spec.rb +0 -127
- data/spec/prawn/svg/parser/path_spec.rb +0 -89
- data/spec/prawn/svg/parser_spec.rb +0 -55
data/lib/prawn/svg/parser.rb
DELETED
@@ -1,268 +0,0 @@
|
|
1
|
-
require 'rexml/document'
|
2
|
-
|
3
|
-
#
|
4
|
-
# Prawn::Svg::Parser is responsible for parsing an SVG file and converting it into a tree of
|
5
|
-
# prawn-compatible method calls.
|
6
|
-
#
|
7
|
-
# You probably do not want to use this class directly. Instead, use Prawn::Svg to draw
|
8
|
-
# SVG data to your Prawn::Document object.
|
9
|
-
#
|
10
|
-
# This class is not passed the prawn object, so knows nothing about
|
11
|
-
# prawn specifically - this might be useful if you want to take this code and use it to convert
|
12
|
-
# SVG to another format.
|
13
|
-
#
|
14
|
-
class Prawn::Svg::Parser
|
15
|
-
CONTAINER_TAGS = %w(g svg symbol defs clipPath)
|
16
|
-
COMMA_WSP_REGEXP = /(?:\s+,?\s*|,\s*)/
|
17
|
-
|
18
|
-
#
|
19
|
-
# Construct a Parser object.
|
20
|
-
#
|
21
|
-
# The +data+ argument is SVG data.
|
22
|
-
#
|
23
|
-
# +bounds+ is a tuple [width, height] that specifies the bounds of the drawing space in points.
|
24
|
-
#
|
25
|
-
# +options+ can optionally contain
|
26
|
-
# the key :width or :height. If both are specified, only :width will be used.
|
27
|
-
#
|
28
|
-
def initialize(document)
|
29
|
-
@document = document
|
30
|
-
end
|
31
|
-
|
32
|
-
#
|
33
|
-
# Parse the SVG data and return a call tree. The returned +Array+ is in the format:
|
34
|
-
#
|
35
|
-
# [
|
36
|
-
# ['prawn_method_name', ['argument1', 'argument2'], []],
|
37
|
-
# ['method_that_takes_a_block', ['argument1', 'argument2'], [
|
38
|
-
# ['method_called_inside_block', ['argument'], []]
|
39
|
-
# ]
|
40
|
-
# ]
|
41
|
-
#
|
42
|
-
def parse
|
43
|
-
@document.warnings.clear
|
44
|
-
|
45
|
-
calls = [['fill_color', '000000', []]]
|
46
|
-
|
47
|
-
calls << [
|
48
|
-
'transformation_matrix',
|
49
|
-
[@document.sizing.x_scale, 0, 0, @document.sizing.y_scale, 0, 0],
|
50
|
-
[]
|
51
|
-
]
|
52
|
-
|
53
|
-
calls << [
|
54
|
-
'transformation_matrix',
|
55
|
-
[1, 0, 0, 1, @document.sizing.x_offset, @document.sizing.y_offset],
|
56
|
-
[]
|
57
|
-
]
|
58
|
-
|
59
|
-
root_element = Prawn::Svg::Element.new(@document, @document.root, calls, :ids => {}, :fill => true)
|
60
|
-
|
61
|
-
parse_element(root_element)
|
62
|
-
calls
|
63
|
-
end
|
64
|
-
|
65
|
-
|
66
|
-
private
|
67
|
-
REQUIRED_ATTRIBUTES = {
|
68
|
-
"polyline" => %w(points),
|
69
|
-
"polygon" => %w(points),
|
70
|
-
"circle" => %w(r),
|
71
|
-
"ellipse" => %w(rx ry),
|
72
|
-
"rect" => %w(width height),
|
73
|
-
"path" => %w(d),
|
74
|
-
"image" => %w(width height)
|
75
|
-
}
|
76
|
-
|
77
|
-
USE_NEW_CIRCLE_CALL = Prawn::Document.new.respond_to?(:circle)
|
78
|
-
USE_NEW_ELLIPSE_CALL = Prawn::Document.new.respond_to?(:ellipse)
|
79
|
-
|
80
|
-
def parse_element(element)
|
81
|
-
attrs = element.attributes
|
82
|
-
|
83
|
-
if required_attributes = REQUIRED_ATTRIBUTES[element.name]
|
84
|
-
return unless check_attrs_present(element, required_attributes)
|
85
|
-
end
|
86
|
-
|
87
|
-
case element.name
|
88
|
-
when *CONTAINER_TAGS
|
89
|
-
do_not_append_calls = %w(symbol defs clipPath).include?(element.name)
|
90
|
-
element.state[:disable_drawing] = true if element.name == "clipPath"
|
91
|
-
|
92
|
-
element.each_child_element do |child|
|
93
|
-
element.add_call "save"
|
94
|
-
parse_element(child)
|
95
|
-
element.add_call "restore"
|
96
|
-
end
|
97
|
-
|
98
|
-
when 'style'
|
99
|
-
load_css_styles(element)
|
100
|
-
|
101
|
-
when 'text'
|
102
|
-
@svg_text ||= Text.new
|
103
|
-
@svg_text.parse(element)
|
104
|
-
|
105
|
-
when 'line'
|
106
|
-
element.add_call 'line', x(attrs['x1'] || '0'), y(attrs['y1'] || '0'), x(attrs['x2'] || '0'), y(attrs['y2'] || '0')
|
107
|
-
|
108
|
-
when 'polyline'
|
109
|
-
points = parse_points(attrs['points'])
|
110
|
-
return unless points.length > 0
|
111
|
-
x, y = points.shift
|
112
|
-
element.add_call 'move_to', x(x), y(y)
|
113
|
-
element.add_call_and_enter 'stroke'
|
114
|
-
points.each do |x, y|
|
115
|
-
element.add_call "line_to", x(x), y(y)
|
116
|
-
end
|
117
|
-
|
118
|
-
when 'polygon'
|
119
|
-
points = parse_points(attrs['points']).collect do |x, y|
|
120
|
-
[x(x), y(y)]
|
121
|
-
end
|
122
|
-
element.add_call "polygon", *points
|
123
|
-
|
124
|
-
when 'circle'
|
125
|
-
xy, r = [x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['r'])
|
126
|
-
|
127
|
-
return if zero_argument?(r)
|
128
|
-
|
129
|
-
if USE_NEW_CIRCLE_CALL
|
130
|
-
element.add_call "circle", xy, r
|
131
|
-
else
|
132
|
-
element.add_call "circle_at", xy, :radius => r
|
133
|
-
end
|
134
|
-
|
135
|
-
when 'ellipse'
|
136
|
-
xy, rx, ry = [x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx'], :x), distance(attrs['ry'], :y)
|
137
|
-
|
138
|
-
return if zero_argument?(rx, ry)
|
139
|
-
|
140
|
-
element.add_call USE_NEW_ELLIPSE_CALL ? "ellipse" : "ellipse_at", xy, rx, ry
|
141
|
-
|
142
|
-
when 'rect'
|
143
|
-
xy = [x(attrs['x'] || '0'), y(attrs['y'] || '0')]
|
144
|
-
width, height = distance(attrs['width'], :x), distance(attrs['height'], :y)
|
145
|
-
radius = distance(attrs['rx'] || attrs['ry'])
|
146
|
-
|
147
|
-
return if zero_argument?(width, height)
|
148
|
-
|
149
|
-
if radius
|
150
|
-
# n.b. does not support both rx and ry being specified with different values
|
151
|
-
element.add_call "rounded_rectangle", xy, width, height, radius
|
152
|
-
else
|
153
|
-
element.add_call "rectangle", xy, width, height
|
154
|
-
end
|
155
|
-
|
156
|
-
when 'path'
|
157
|
-
parse_path(element)
|
158
|
-
|
159
|
-
when 'use'
|
160
|
-
parse_use(element)
|
161
|
-
|
162
|
-
when 'title', 'desc', 'metadata'
|
163
|
-
# ignore
|
164
|
-
do_not_append_calls = true
|
165
|
-
|
166
|
-
when 'font-face'
|
167
|
-
# not supported
|
168
|
-
do_not_append_calls = true
|
169
|
-
|
170
|
-
when 'image'
|
171
|
-
@svg_image ||= Image.new(@document)
|
172
|
-
@svg_image.parse(element)
|
173
|
-
|
174
|
-
else
|
175
|
-
@document.warnings << "Unknown tag '#{element.name}'; ignoring"
|
176
|
-
end
|
177
|
-
|
178
|
-
element.append_calls_to_parent unless do_not_append_calls
|
179
|
-
end
|
180
|
-
|
181
|
-
|
182
|
-
def parse_path(element)
|
183
|
-
@svg_path ||= Path.new
|
184
|
-
|
185
|
-
begin
|
186
|
-
commands = @svg_path.parse(element.attributes['d'])
|
187
|
-
rescue Prawn::Svg::Parser::Path::InvalidError => e
|
188
|
-
commands = []
|
189
|
-
@document.warnings << e.message
|
190
|
-
end
|
191
|
-
|
192
|
-
element.add_call 'join_style', :bevel
|
193
|
-
|
194
|
-
commands.collect do |command, args|
|
195
|
-
if args && args.length > 0
|
196
|
-
point_to = [x(args[0]), y(args[1])]
|
197
|
-
if command == 'curve_to'
|
198
|
-
opts = {:bounds => [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]}
|
199
|
-
end
|
200
|
-
element.add_call command, point_to, opts
|
201
|
-
else
|
202
|
-
element.add_call command
|
203
|
-
end
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
def parse_use(element)
|
208
|
-
if href = element.attributes['xlink:href']
|
209
|
-
if href[0..0] == '#'
|
210
|
-
id = href[1..-1]
|
211
|
-
if definition_element = @document.elements_by_id[id]
|
212
|
-
x = element.attributes['x']
|
213
|
-
y = element.attributes['y']
|
214
|
-
if x || y
|
215
|
-
element.add_call_and_enter "translate", distance(x || 0, :x), -distance(y || 0, :y)
|
216
|
-
end
|
217
|
-
element.add_calls_from_element definition_element
|
218
|
-
else
|
219
|
-
@document.warnings << "no tag with ID '#{id}' was found, referenced by use tag"
|
220
|
-
end
|
221
|
-
else
|
222
|
-
@document.warnings << "use tag has an href that is not a reference to an id; this is not supported"
|
223
|
-
end
|
224
|
-
else
|
225
|
-
@document.warnings << "no xlink:href specified on use tag"
|
226
|
-
end
|
227
|
-
end
|
228
|
-
|
229
|
-
####################################################################################################################
|
230
|
-
|
231
|
-
def load_css_styles(element)
|
232
|
-
if @document.css_parser
|
233
|
-
data = if element.element.cdatas.any?
|
234
|
-
element.element.cdatas.collect {|d| d.to_s}.join
|
235
|
-
else
|
236
|
-
element.element.text
|
237
|
-
end
|
238
|
-
|
239
|
-
@document.css_parser.add_block!(data)
|
240
|
-
end
|
241
|
-
end
|
242
|
-
|
243
|
-
def check_attrs_present(element, attrs)
|
244
|
-
missing_attrs = attrs - element.attributes.keys
|
245
|
-
if missing_attrs.any?
|
246
|
-
@document.warnings << "Must have attributes #{missing_attrs.join(", ")} on tag #{element.name}; skipping tag"
|
247
|
-
end
|
248
|
-
missing_attrs.empty?
|
249
|
-
end
|
250
|
-
|
251
|
-
def zero_argument?(*args)
|
252
|
-
args.any? {|arg| arg.nil? || arg <= 0}
|
253
|
-
end
|
254
|
-
|
255
|
-
%w(x y distance).each do |method|
|
256
|
-
define_method(method) {|*a| @document.send(method, *a)}
|
257
|
-
end
|
258
|
-
|
259
|
-
def parse_points(points_string)
|
260
|
-
points_string.
|
261
|
-
to_s.
|
262
|
-
strip.
|
263
|
-
gsub(/(\d)-(\d)/, '\1 -\2').
|
264
|
-
split(COMMA_WSP_REGEXP).
|
265
|
-
each_slice(2).
|
266
|
-
to_a
|
267
|
-
end
|
268
|
-
end
|
@@ -1,81 +0,0 @@
|
|
1
|
-
class Prawn::Svg::Parser::Image
|
2
|
-
Error = Class.new(StandardError)
|
3
|
-
|
4
|
-
class FakeIO
|
5
|
-
def initialize(data)
|
6
|
-
@data = data
|
7
|
-
end
|
8
|
-
def read
|
9
|
-
@data
|
10
|
-
end
|
11
|
-
def rewind
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def initialize(document)
|
16
|
-
@document = document
|
17
|
-
@url_cache = {}
|
18
|
-
end
|
19
|
-
|
20
|
-
def parse(element)
|
21
|
-
return if element.state[:display] == "none"
|
22
|
-
|
23
|
-
attrs = element.attributes
|
24
|
-
url = attrs['xlink:href'] || attrs['href']
|
25
|
-
if url.nil?
|
26
|
-
raise Error, "image tag must have an xlink:href"
|
27
|
-
end
|
28
|
-
|
29
|
-
if !@document.url_loader.valid?(url)
|
30
|
-
raise Error, "image tag xlink:href attribute must use http, https or data scheme"
|
31
|
-
end
|
32
|
-
|
33
|
-
image = begin
|
34
|
-
@document.url_loader.load(url)
|
35
|
-
rescue => e
|
36
|
-
raise Error, "Error retrieving URL #{url}: #{e.message}"
|
37
|
-
end
|
38
|
-
|
39
|
-
x = x(attrs['x'] || 0)
|
40
|
-
y = y(attrs['y'] || 0)
|
41
|
-
width = distance(attrs['width'])
|
42
|
-
height = distance(attrs['height'])
|
43
|
-
|
44
|
-
return if width.zero? || height.zero?
|
45
|
-
raise Error, "width and height must be 0 or higher" if width < 0 || height < 0
|
46
|
-
|
47
|
-
aspect = Prawn::Svg::Calculators::AspectRatio.new(attrs['preserveAspectRatio'], [width, height], image_dimensions(image))
|
48
|
-
|
49
|
-
if aspect.slice?
|
50
|
-
element.add_call "save"
|
51
|
-
element.add_call "rectangle", [x, y], width, height
|
52
|
-
element.add_call "clip"
|
53
|
-
end
|
54
|
-
|
55
|
-
options = {:width => aspect.width, :height => aspect.height, :at => [x + aspect.x, y - aspect.y]}
|
56
|
-
|
57
|
-
element.add_call "image", FakeIO.new(image), options
|
58
|
-
element.add_call "restore" if aspect.slice?
|
59
|
-
rescue Error => e
|
60
|
-
@document.warnings << e.message
|
61
|
-
end
|
62
|
-
|
63
|
-
|
64
|
-
protected
|
65
|
-
def image_dimensions(data)
|
66
|
-
handler = if data[0, 3].unpack("C*") == [255, 216, 255]
|
67
|
-
Prawn::Images::JPG
|
68
|
-
elsif data[0, 8].unpack("C*") == [137, 80, 78, 71, 13, 10, 26, 10]
|
69
|
-
Prawn::Images::PNG
|
70
|
-
else
|
71
|
-
raise Error, "Unsupported image type supplied to image tag; Prawn only supports JPG and PNG"
|
72
|
-
end
|
73
|
-
|
74
|
-
image = handler.new(data)
|
75
|
-
[image.width.to_f, image.height.to_f]
|
76
|
-
end
|
77
|
-
|
78
|
-
%w(x y distance).each do |method|
|
79
|
-
define_method(method) {|*a| @document.send(method, *a)}
|
80
|
-
end
|
81
|
-
end
|
@@ -1,392 +0,0 @@
|
|
1
|
-
module Prawn
|
2
|
-
module Svg
|
3
|
-
class Parser::Path
|
4
|
-
# Raised if the SVG path cannot be parsed.
|
5
|
-
InvalidError = Class.new(StandardError)
|
6
|
-
|
7
|
-
INSIDE_SPACE_REGEXP = /[ \t\r\n,]*/
|
8
|
-
OUTSIDE_SPACE_REGEXP = /[ \t\r\n]*/
|
9
|
-
INSIDE_REGEXP = /#{INSIDE_SPACE_REGEXP}([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:(?<=[0-9])e[+-]?[0-9]+)?)/
|
10
|
-
VALUES_REGEXP = /^#{INSIDE_REGEXP}/
|
11
|
-
COMMAND_REGEXP = /^#{OUTSIDE_SPACE_REGEXP}([A-Za-z])((?:#{INSIDE_REGEXP})*)#{OUTSIDE_SPACE_REGEXP}/
|
12
|
-
|
13
|
-
FLOAT_ERROR_DELTA = 1e-10
|
14
|
-
|
15
|
-
#
|
16
|
-
# Parses an SVG path and returns a Prawn-compatible call tree.
|
17
|
-
#
|
18
|
-
def parse(data)
|
19
|
-
@subpath_initial_point = @last_point = nil
|
20
|
-
@previous_control_point = @previous_quadratic_control_point = nil
|
21
|
-
@calls = []
|
22
|
-
|
23
|
-
data = data.gsub(/#{OUTSIDE_SPACE_REGEXP}$/, '')
|
24
|
-
|
25
|
-
matched_commands = match_all(data, COMMAND_REGEXP)
|
26
|
-
raise InvalidError, "Invalid/unsupported syntax for SVG path data" if matched_commands.nil?
|
27
|
-
|
28
|
-
matched_commands.each do |matched_command|
|
29
|
-
command = matched_command[1]
|
30
|
-
matched_values = match_all(matched_command[2], VALUES_REGEXP)
|
31
|
-
raise "should be impossible to have invalid inside data, but we ended up here" if matched_values.nil?
|
32
|
-
values = matched_values.collect {|value| value[1].to_f}
|
33
|
-
run_path_command(command, values)
|
34
|
-
end
|
35
|
-
|
36
|
-
@calls
|
37
|
-
end
|
38
|
-
|
39
|
-
|
40
|
-
private
|
41
|
-
def run_path_command(command, values)
|
42
|
-
upcase_command = command.upcase
|
43
|
-
relative = command != upcase_command
|
44
|
-
|
45
|
-
case upcase_command
|
46
|
-
when 'M' # moveto
|
47
|
-
x = values.shift
|
48
|
-
y = values.shift
|
49
|
-
|
50
|
-
if relative && @last_point
|
51
|
-
x += @last_point.first
|
52
|
-
y += @last_point.last
|
53
|
-
end
|
54
|
-
|
55
|
-
@last_point = @subpath_initial_point = [x, y]
|
56
|
-
@calls << ["move_to", @last_point]
|
57
|
-
|
58
|
-
return run_path_command(relative ? 'l' : 'L', values) if values.any?
|
59
|
-
|
60
|
-
when 'Z' # closepath
|
61
|
-
if @subpath_initial_point
|
62
|
-
#@calls << ["line_to", @subpath_initial_point]
|
63
|
-
@calls << ["close_path"]
|
64
|
-
@last_point = @subpath_initial_point
|
65
|
-
end
|
66
|
-
|
67
|
-
when 'L' # lineto
|
68
|
-
while values.any?
|
69
|
-
x = values.shift
|
70
|
-
y = values.shift
|
71
|
-
if relative && @last_point
|
72
|
-
x += @last_point.first
|
73
|
-
y += @last_point.last
|
74
|
-
end
|
75
|
-
@last_point = [x, y]
|
76
|
-
@calls << ["line_to", @last_point]
|
77
|
-
end
|
78
|
-
|
79
|
-
when 'H' # horizontal lineto
|
80
|
-
while values.any?
|
81
|
-
x = values.shift
|
82
|
-
x += @last_point.first if relative && @last_point
|
83
|
-
@last_point = [x, @last_point.last]
|
84
|
-
@calls << ["line_to", @last_point]
|
85
|
-
end
|
86
|
-
|
87
|
-
when 'V' # vertical lineto
|
88
|
-
while values.any?
|
89
|
-
y = values.shift
|
90
|
-
y += @last_point.last if relative && @last_point
|
91
|
-
@last_point = [@last_point.first, y]
|
92
|
-
@calls << ["line_to", @last_point]
|
93
|
-
end
|
94
|
-
|
95
|
-
when 'C' # curveto
|
96
|
-
while values.any?
|
97
|
-
x1, y1, x2, y2, x, y = (1..6).collect {values.shift}
|
98
|
-
if relative && @last_point
|
99
|
-
x += @last_point.first
|
100
|
-
x1 += @last_point.first
|
101
|
-
x2 += @last_point.first
|
102
|
-
y += @last_point.last
|
103
|
-
y1 += @last_point.last
|
104
|
-
y2 += @last_point.last
|
105
|
-
end
|
106
|
-
|
107
|
-
@last_point = [x, y]
|
108
|
-
@previous_control_point = [x2, y2]
|
109
|
-
@calls << ["curve_to", [x, y, x1, y1, x2, y2]]
|
110
|
-
end
|
111
|
-
|
112
|
-
when 'S' # shorthand/smooth curveto
|
113
|
-
while values.any?
|
114
|
-
x2, y2, x, y = (1..4).collect {values.shift}
|
115
|
-
if relative && @last_point
|
116
|
-
x += @last_point.first
|
117
|
-
x2 += @last_point.first
|
118
|
-
y += @last_point.last
|
119
|
-
y2 += @last_point.last
|
120
|
-
end
|
121
|
-
|
122
|
-
if @previous_control_point
|
123
|
-
x1 = 2 * @last_point.first - @previous_control_point.first
|
124
|
-
y1 = 2 * @last_point.last - @previous_control_point.last
|
125
|
-
else
|
126
|
-
x1, y1 = @last_point
|
127
|
-
end
|
128
|
-
|
129
|
-
@last_point = [x, y]
|
130
|
-
@previous_control_point = [x2, y2]
|
131
|
-
@calls << ["curve_to", [x, y, x1, y1, x2, y2]]
|
132
|
-
end
|
133
|
-
|
134
|
-
when 'Q', 'T' # quadratic curveto
|
135
|
-
while values.any?
|
136
|
-
if shorthand = upcase_command == 'T'
|
137
|
-
x, y = (1..2).collect {values.shift}
|
138
|
-
else
|
139
|
-
x1, y1, x, y = (1..4).collect {values.shift}
|
140
|
-
end
|
141
|
-
|
142
|
-
if relative && @last_point
|
143
|
-
x += @last_point.first
|
144
|
-
x1 += @last_point.first if x1
|
145
|
-
y += @last_point.last
|
146
|
-
y1 += @last_point.last if y1
|
147
|
-
end
|
148
|
-
|
149
|
-
if shorthand
|
150
|
-
if @previous_quadratic_control_point
|
151
|
-
x1 = 2 * @last_point.first - @previous_quadratic_control_point.first
|
152
|
-
y1 = 2 * @last_point.last - @previous_quadratic_control_point.last
|
153
|
-
else
|
154
|
-
x1, y1 = @last_point
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
# convert from quadratic to cubic
|
159
|
-
cx1 = @last_point.first + (x1 - @last_point.first) * 2 / 3.0
|
160
|
-
cy1 = @last_point.last + (y1 - @last_point.last) * 2 / 3.0
|
161
|
-
cx2 = cx1 + (x - @last_point.first) / 3.0
|
162
|
-
cy2 = cy1 + (y - @last_point.last) / 3.0
|
163
|
-
|
164
|
-
@last_point = [x, y]
|
165
|
-
@previous_quadratic_control_point = [x1, y1]
|
166
|
-
|
167
|
-
@calls << ["curve_to", [x, y, cx1, cy1, cx2, cy2]]
|
168
|
-
end
|
169
|
-
|
170
|
-
when 'A'
|
171
|
-
return unless @last_point
|
172
|
-
|
173
|
-
while values.any?
|
174
|
-
rx, ry, phi, fa, fs, x2, y2 = (1..7).collect {values.shift}
|
175
|
-
x1, y1 = @last_point
|
176
|
-
|
177
|
-
return if rx.zero? && ry.zero?
|
178
|
-
|
179
|
-
if relative
|
180
|
-
x2 += x1
|
181
|
-
y2 += y1
|
182
|
-
end
|
183
|
-
|
184
|
-
# Normalise values as per F.6.2
|
185
|
-
rx = rx.abs
|
186
|
-
ry = ry.abs
|
187
|
-
phi = (phi % 360) * 2 * Math::PI / 360.0
|
188
|
-
|
189
|
-
# F.6.2: If the endpoints (x1, y1) and (x2, y2) are identical, then this is equivalent to omitting the elliptical arc segment entirely.
|
190
|
-
return if within_float_delta?(x1, x2) && within_float_delta?(y1, y2)
|
191
|
-
|
192
|
-
# F.6.2: If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a "lineto") joining the endpoints.
|
193
|
-
if within_float_delta?(rx, 0) || within_float_delta?(ry, 0)
|
194
|
-
@last_point = [x2, y2]
|
195
|
-
@calls << ["line_to", @last_point]
|
196
|
-
return
|
197
|
-
end
|
198
|
-
|
199
|
-
# We need to get the center co-ordinates, as well as the angles from the X axis to the start and end
|
200
|
-
# points. To do this, we use the algorithm documented in the SVG specification section F.6.5.
|
201
|
-
|
202
|
-
# F.6.5.1
|
203
|
-
xp1 = Math.cos(phi) * ((x1-x2)/2.0) + Math.sin(phi) * ((y1-y2)/2.0)
|
204
|
-
yp1 = -Math.sin(phi) * ((x1-x2)/2.0) + Math.cos(phi) * ((y1-y2)/2.0)
|
205
|
-
|
206
|
-
# F.6.6.2
|
207
|
-
r2x = rx * rx
|
208
|
-
r2y = ry * ry
|
209
|
-
hat = xp1 * xp1 / r2x + yp1 * yp1 / r2y
|
210
|
-
if hat > 1
|
211
|
-
rx *= Math.sqrt(hat)
|
212
|
-
ry *= Math.sqrt(hat)
|
213
|
-
end
|
214
|
-
|
215
|
-
# F.6.5.2
|
216
|
-
r2x = rx * rx
|
217
|
-
r2y = ry * ry
|
218
|
-
square = (r2x * r2y - r2x * yp1 * yp1 - r2y * xp1 * xp1) / (r2x * yp1 * yp1 + r2y * xp1 * xp1)
|
219
|
-
square = 0 if square < 0 && square > -FLOAT_ERROR_DELTA # catch rounding errors
|
220
|
-
base = Math.sqrt(square)
|
221
|
-
base *= -1 if fa == fs
|
222
|
-
cpx = base * rx * yp1 / ry
|
223
|
-
cpy = base * -ry * xp1 / rx
|
224
|
-
|
225
|
-
# F.6.5.3
|
226
|
-
cx = Math.cos(phi) * cpx + -Math.sin(phi) * cpy + (x1 + x2) / 2
|
227
|
-
cy = Math.sin(phi) * cpx + Math.cos(phi) * cpy + (y1 + y2) / 2
|
228
|
-
|
229
|
-
# F.6.5.5
|
230
|
-
vx = (xp1 - cpx) / rx
|
231
|
-
vy = (yp1 - cpy) / ry
|
232
|
-
theta_1 = Math.acos(vx / Math.sqrt(vx * vx + vy * vy))
|
233
|
-
theta_1 *= -1 if vy < 0
|
234
|
-
|
235
|
-
# F.6.5.6
|
236
|
-
ux = vx
|
237
|
-
uy = vy
|
238
|
-
vx = (-xp1 - cpx) / rx
|
239
|
-
vy = (-yp1 - cpy) / ry
|
240
|
-
|
241
|
-
numerator = ux * vx + uy * vy
|
242
|
-
denominator = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy)
|
243
|
-
division = numerator / denominator
|
244
|
-
division = -1 if division < -1 # for rounding errors
|
245
|
-
|
246
|
-
d_theta = Math.acos(division) % (2 * Math::PI)
|
247
|
-
d_theta *= -1 if ux * vy - uy * vx < 0
|
248
|
-
|
249
|
-
# Adjust range
|
250
|
-
if fs == 0
|
251
|
-
d_theta -= 2 * Math::PI if d_theta > 0
|
252
|
-
else
|
253
|
-
d_theta += 2 * Math::PI if d_theta < 0
|
254
|
-
end
|
255
|
-
|
256
|
-
theta_2 = theta_1 + d_theta
|
257
|
-
|
258
|
-
calculate_bezier_curve_points_for_arc(cx, cy, rx, ry, theta_1, theta_2, phi).each do |points|
|
259
|
-
@calls << ["curve_to", points[:p2] + points[:q1] + points[:q2]]
|
260
|
-
@last_point = points[:p2]
|
261
|
-
end
|
262
|
-
end
|
263
|
-
end
|
264
|
-
|
265
|
-
@previous_control_point = nil unless %w(C S).include?(upcase_command)
|
266
|
-
@previous_quadratic_control_point = nil unless %w(Q T).include?(upcase_command)
|
267
|
-
end
|
268
|
-
|
269
|
-
def within_float_delta?(a, b)
|
270
|
-
(a - b).abs < FLOAT_ERROR_DELTA
|
271
|
-
end
|
272
|
-
|
273
|
-
def match_all(string, regexp) # regexp must start with ^
|
274
|
-
result = []
|
275
|
-
while string != ""
|
276
|
-
matches = string.match(regexp)
|
277
|
-
result << matches
|
278
|
-
return if matches.nil?
|
279
|
-
string = matches.post_match
|
280
|
-
end
|
281
|
-
result
|
282
|
-
end
|
283
|
-
|
284
|
-
def calculate_eta_from_lambda(a, b, lambda_1, lambda_2)
|
285
|
-
# 2.2.1
|
286
|
-
eta1 = Math.atan2(Math.sin(lambda_1) / b, Math.cos(lambda_1) / a)
|
287
|
-
eta2 = Math.atan2(Math.sin(lambda_2) / b, Math.cos(lambda_2) / a)
|
288
|
-
|
289
|
-
# ensure eta1 <= eta2 <= eta1 + 2*PI
|
290
|
-
eta2 -= 2 * Math::PI * ((eta2 - eta1) / (2 * Math::PI)).floor
|
291
|
-
eta2 += 2 * Math::PI if lambda_2 - lambda_1 > Math::PI && eta2 - eta1 < Math::PI
|
292
|
-
|
293
|
-
[eta1, eta2]
|
294
|
-
end
|
295
|
-
|
296
|
-
# Convert the elliptical arc to a cubic bézier curve using this algorithm:
|
297
|
-
# http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
|
298
|
-
def calculate_bezier_curve_points_for_arc(cx, cy, a, b, lambda_1, lambda_2, theta)
|
299
|
-
e = lambda do |eta|
|
300
|
-
[
|
301
|
-
cx + a * Math.cos(theta) * Math.cos(eta) - b * Math.sin(theta) * Math.sin(eta),
|
302
|
-
cy + a * Math.sin(theta) * Math.cos(eta) + b * Math.cos(theta) * Math.sin(eta)
|
303
|
-
]
|
304
|
-
end
|
305
|
-
|
306
|
-
ep = lambda do |eta|
|
307
|
-
[
|
308
|
-
-a * Math.cos(theta) * Math.sin(eta) - b * Math.sin(theta) * Math.cos(eta),
|
309
|
-
-a * Math.sin(theta) * Math.sin(eta) + b * Math.cos(theta) * Math.cos(eta)
|
310
|
-
]
|
311
|
-
end
|
312
|
-
|
313
|
-
iterations = 1
|
314
|
-
d_lambda = lambda_2 - lambda_1
|
315
|
-
|
316
|
-
while iterations < 1024
|
317
|
-
if d_lambda.abs <= Math::PI / 2.0
|
318
|
-
# TODO : run error algorithm, see whether it meets threshold or not
|
319
|
-
# puts "error = #{calculate_curve_approximation_error(a, b, eta1, eta1 + d_eta)}"
|
320
|
-
break
|
321
|
-
end
|
322
|
-
iterations *= 2
|
323
|
-
d_lambda = (lambda_2 - lambda_1) / iterations
|
324
|
-
end
|
325
|
-
|
326
|
-
(0...iterations).collect do |iteration|
|
327
|
-
eta_a, eta_b = calculate_eta_from_lambda(a, b, lambda_1+iteration*d_lambda, lambda_1+(iteration+1)*d_lambda)
|
328
|
-
d_eta = eta_b - eta_a
|
329
|
-
|
330
|
-
alpha = Math.sin(d_eta) * ((Math.sqrt(4 + 3 * Math.tan(d_eta / 2) ** 2) - 1) / 3)
|
331
|
-
|
332
|
-
x1, y1 = e[eta_a]
|
333
|
-
x2, y2 = e[eta_b]
|
334
|
-
|
335
|
-
ep_eta1_x, ep_eta1_y = ep[eta_a]
|
336
|
-
q1_x = x1 + alpha * ep_eta1_x
|
337
|
-
q1_y = y1 + alpha * ep_eta1_y
|
338
|
-
|
339
|
-
ep_eta2_x, ep_eta2_y = ep[eta_b]
|
340
|
-
q2_x = x2 - alpha * ep_eta2_x
|
341
|
-
q2_y = y2 - alpha * ep_eta2_y
|
342
|
-
|
343
|
-
{:p2 => [x2, y2], :q1 => [q1_x, q1_y], :q2 => [q2_x, q2_y]}
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
ERROR_COEFFICIENTS_A = [
|
348
|
-
[
|
349
|
-
[3.85268, -21.229, -0.330434, 0.0127842],
|
350
|
-
[-1.61486, 0.706564, 0.225945, 0.263682],
|
351
|
-
[-0.910164, 0.388383, 0.00551445, 0.00671814],
|
352
|
-
[-0.630184, 0.192402, 0.0098871, 0.0102527]
|
353
|
-
],
|
354
|
-
[
|
355
|
-
[-0.162211, 9.94329, 0.13723, 0.0124084],
|
356
|
-
[-0.253135, 0.00187735, 0.0230286, 0.01264],
|
357
|
-
[-0.0695069, -0.0437594, 0.0120636, 0.0163087],
|
358
|
-
[-0.0328856, -0.00926032, -0.00173573, 0.00527385]
|
359
|
-
]
|
360
|
-
]
|
361
|
-
|
362
|
-
ERROR_COEFFICIENTS_B = [
|
363
|
-
[
|
364
|
-
[0.0899116, -19.2349, -4.11711, 0.183362],
|
365
|
-
[0.138148, -1.45804, 1.32044, 1.38474],
|
366
|
-
[0.230903, -0.450262, 0.219963, 0.414038],
|
367
|
-
[0.0590565, -0.101062, 0.0430592, 0.0204699]
|
368
|
-
],
|
369
|
-
[
|
370
|
-
[0.0164649, 9.89394, 0.0919496, 0.00760802],
|
371
|
-
[0.0191603, -0.0322058, 0.0134667, -0.0825018],
|
372
|
-
[0.0156192, -0.017535, 0.00326508, -0.228157],
|
373
|
-
[-0.0236752, 0.0405821, -0.0173086, 0.176187]
|
374
|
-
]
|
375
|
-
]
|
376
|
-
|
377
|
-
def calculate_curve_approximation_error(a, b, eta1, eta2)
|
378
|
-
b_over_a = b / a
|
379
|
-
coefficents = b_over_a < 0.25 ? ERROR_COEFFICIENTS_A : ERROR_COEFFICIENTS_B
|
380
|
-
|
381
|
-
c = lambda do |i|
|
382
|
-
(0..3).inject(0) do |accumulator, j|
|
383
|
-
coef = coefficents[i][j]
|
384
|
-
accumulator + ((coef[0] * b_over_a**2 + coef[1] * b_over_a + coef[2]) / (b_over_a * coef[3])) * Math.cos(j * (eta1 + eta2))
|
385
|
-
end
|
386
|
-
end
|
387
|
-
|
388
|
-
((0.001 * b_over_a**2 + 4.98 * b_over_a + 0.207) / (b_over_a * 0.0067)) * a * Math.exp(c[0] + c[1] * (eta2 - eta1))
|
389
|
-
end
|
390
|
-
end
|
391
|
-
end
|
392
|
-
end
|