prawn-svg 0.15.0.0 → 0.23.0

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