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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -0
  3. data/README.md +11 -4
  4. data/lib/prawn-svg.rb +9 -6
  5. data/lib/prawn/svg/attributes.rb +6 -0
  6. data/lib/prawn/svg/attributes/clip_path.rb +17 -0
  7. data/lib/prawn/svg/attributes/display.rb +5 -0
  8. data/lib/prawn/svg/attributes/font.rb +38 -0
  9. data/lib/prawn/svg/attributes/opacity.rb +15 -0
  10. data/lib/prawn/svg/attributes/stroke.rb +35 -0
  11. data/lib/prawn/svg/attributes/transform.rb +50 -0
  12. data/lib/prawn/svg/calculators/aspect_ratio.rb +1 -1
  13. data/lib/prawn/svg/calculators/document_sizing.rb +2 -2
  14. data/lib/prawn/svg/calculators/pixels.rb +1 -1
  15. data/lib/prawn/svg/color.rb +44 -14
  16. data/lib/prawn/svg/document.rb +6 -5
  17. data/lib/prawn/svg/elements.rb +33 -0
  18. data/lib/prawn/svg/elements/base.rb +228 -0
  19. data/lib/prawn/svg/elements/circle.rb +25 -0
  20. data/lib/prawn/svg/elements/container.rb +15 -0
  21. data/lib/prawn/svg/elements/ellipse.rb +23 -0
  22. data/lib/prawn/svg/elements/gradient.rb +117 -0
  23. data/lib/prawn/svg/elements/ignored.rb +5 -0
  24. data/lib/prawn/svg/elements/image.rb +85 -0
  25. data/lib/prawn/svg/elements/line.rb +16 -0
  26. data/lib/prawn/svg/elements/path.rb +405 -0
  27. data/lib/prawn/svg/elements/polygon.rb +17 -0
  28. data/lib/prawn/svg/elements/polyline.rb +22 -0
  29. data/lib/prawn/svg/elements/rect.rb +33 -0
  30. data/lib/prawn/svg/elements/root.rb +9 -0
  31. data/lib/prawn/svg/elements/style.rb +10 -0
  32. data/lib/prawn/svg/elements/text.rb +87 -0
  33. data/lib/prawn/svg/elements/use.rb +29 -0
  34. data/lib/prawn/svg/extension.rb +2 -2
  35. data/lib/prawn/svg/font.rb +3 -3
  36. data/lib/prawn/svg/interface.rb +12 -5
  37. data/lib/prawn/svg/url_loader.rb +1 -1
  38. data/lib/prawn/svg/version.rb +2 -2
  39. data/prawn-svg.gemspec +3 -3
  40. data/spec/integration_spec.rb +59 -2
  41. data/spec/prawn/svg/attributes/font_spec.rb +49 -0
  42. data/spec/prawn/svg/attributes/transform_spec.rb +56 -0
  43. data/spec/prawn/svg/calculators/aspect_ratio_spec.rb +2 -2
  44. data/spec/prawn/svg/calculators/document_sizing_spec.rb +3 -3
  45. data/spec/prawn/svg/color_spec.rb +36 -15
  46. data/spec/prawn/svg/document_spec.rb +4 -4
  47. data/spec/prawn/svg/elements/base_spec.rb +125 -0
  48. data/spec/prawn/svg/elements/gradient_spec.rb +61 -0
  49. data/spec/prawn/svg/elements/path_spec.rb +123 -0
  50. data/spec/prawn/svg/elements/style_spec.rb +23 -0
  51. data/spec/prawn/svg/{parser → elements}/text_spec.rb +7 -8
  52. data/spec/prawn/svg/font_spec.rb +12 -12
  53. data/spec/prawn/svg/interface_spec.rb +7 -7
  54. data/spec/prawn/svg/url_loader_spec.rb +2 -2
  55. data/spec/sample_svg/gradients.svg +40 -0
  56. data/spec/sample_svg/rect02.svg +8 -11
  57. data/spec/spec_helper.rb +1 -1
  58. metadata +46 -18
  59. data/lib/prawn/svg/element.rb +0 -304
  60. data/lib/prawn/svg/parser.rb +0 -268
  61. data/lib/prawn/svg/parser/image.rb +0 -81
  62. data/lib/prawn/svg/parser/path.rb +0 -392
  63. data/lib/prawn/svg/parser/text.rb +0 -80
  64. data/spec/prawn/svg/element_spec.rb +0 -127
  65. data/spec/prawn/svg/parser/path_spec.rb +0 -89
  66. data/spec/prawn/svg/parser_spec.rb +0 -55
@@ -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