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