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.
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