prawn-svg 0.9.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.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright 2010 Roger Nesbitt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,17 @@
1
+ The very start of an SVG renderer for Prawn.
2
+
3
+ This will take an SVG file as input and render it into your PDF. Find out more about the Prawn PDF library at:
4
+
5
+ http://wiki.github.com/sandal/prawn/
6
+
7
+ Using prawn-svg:
8
+
9
+ Prawn::Document.generate("svg.pdf") do
10
+ svg svg_data, :at => [x, y], :width => w
11
+ end
12
+
13
+ :at must be specified. :width, :height, or neither may be specified; if neither is present,
14
+ the resolution specified in the SVG will be used.
15
+
16
+ Note that only a very small subset of SVG is currently supported. It's just enough so that
17
+ it renders a simple graph made by Scruffy.
data/lib/prawn-svg.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'prawn/svg_document'
2
+ require 'prawn/svg/svg'
3
+ require 'prawn/svg/svg_path'
@@ -0,0 +1,320 @@
1
+ require 'rexml/document'
2
+ require 'prawn'
3
+
4
+ class Prawn::Svg
5
+ include Prawn::Measurements
6
+
7
+ attr_reader :data, :prawn, :options
8
+ attr_accessor :scale
9
+
10
+ def initialize(data, prawn, options)
11
+ @data = data
12
+ @prawn = prawn
13
+ @options = options
14
+ end
15
+
16
+ def draw
17
+ root = parse_document
18
+ calculate_dimensions
19
+
20
+ prawn.bounding_box(@options[:at], :width => @width, :height => @height) do
21
+ prawn.save_graphics_state do
22
+ call_tree = generate_call_tree(root)
23
+ proc_creator(prawn, call_tree).call
24
+ end
25
+ end
26
+ end
27
+
28
+ def generate_call_tree(element)
29
+ [].tap {|calls| parse_element(element, calls, {})}
30
+ end
31
+
32
+
33
+ protected
34
+ def proc_creator(prawn, calls)
35
+ Proc.new {issue_prawn_command(prawn, calls)}
36
+ end
37
+
38
+ def issue_prawn_command(prawn, calls)
39
+ calls.each do |call, arguments, children|
40
+ if children.empty?
41
+ rewrite_call_arguments(prawn, call, arguments)
42
+ prawn.send(call, *arguments)
43
+ else
44
+ prawn.send(call, *arguments, &proc_creator(prawn, children))
45
+ end
46
+ end
47
+ end
48
+
49
+ def rewrite_call_arguments(prawn, call, arguments)
50
+ if call == 'text_box'
51
+ if (anchor = arguments.last.delete(:text_anchor)) && %w(middle end).include?(anchor)
52
+ width = prawn.width_of(*arguments)
53
+ width /= 2 if anchor == 'middle'
54
+ arguments.last[:at][0] -= width
55
+ end
56
+
57
+ arguments.last[:at][1] += prawn.height_of(*arguments) / 3 * 2
58
+ end
59
+ end
60
+
61
+ def parse_document
62
+ REXML::Document.new(@data).root.tap do |root|
63
+ if vb = root.attributes['viewBox']
64
+ x1, y1, x2, y2 = vb.strip.split(/\s+/)
65
+ @x_offset, @y_offset = [x1.to_f, y1.to_f]
66
+ @actual_width, @actual_height = [x2.to_f - x1.to_f, y2.to_f - y1.to_f]
67
+ else
68
+ @x_offset, @y_offset = [0, 0]
69
+ @actual_width = root.attributes['width'].to_f
70
+ @actual_height = root.attributes['height'].to_f
71
+ end
72
+ end
73
+ end
74
+
75
+ def parse_element(element, calls, state)
76
+ attrs = element.attributes
77
+
78
+ if transform = attrs['transform']
79
+ parse_css_method_calls(transform).each do |name, arguments|
80
+ case name
81
+ when 'translate'
82
+ x, y = arguments
83
+ x, y = x.split(/\s+/) if y.nil?
84
+ calls << [name, [distance(x), -distance(y)], []]
85
+ calls = calls.last.last
86
+ when 'rotate'
87
+ calls << [name, [-arguments.first.to_f, {:origin => [0, y('0')]}], []]
88
+ calls = calls.last.last
89
+ when 'scale'
90
+ calls << [name, [arguments.first.to_f], []]
91
+ calls = calls.last.last
92
+ else
93
+ #raise "unknown transformation '#{name}'"
94
+ end
95
+ end
96
+ end
97
+
98
+ calls, style_attrs, draw_type = apply_styles(attrs, calls, state)
99
+
100
+ state[:draw_type] = draw_type if draw_type != ""
101
+ if state[:draw_type] && !%w(g svg).include?(element.name)
102
+ calls << [state[:draw_type], [], []]
103
+ calls = calls.last.last
104
+ end
105
+
106
+ case element.name
107
+ when 'defs', 'desc'
108
+ # ignore these tags
109
+
110
+ when 'g', 'svg'
111
+ element.elements.each do |child|
112
+ parse_element(child, calls, state.dup)
113
+ end
114
+
115
+ when 'text'
116
+ # very primitive support for fonts
117
+ if (font = style_attrs['font-family']) && !font.match(/[\/\\]/)
118
+ font = font.strip
119
+ if font != ""
120
+ calls << ['font', [font], []]
121
+ calls = calls.last.last
122
+ end
123
+ end
124
+
125
+ opts = {:at => [x(attrs['x']), y(attrs['y'])]}
126
+ if size = style_attrs['font-size']
127
+ opts[:size] = size.to_f * @scale
128
+ end
129
+
130
+ # This is not a prawn option but we can't work out how to render it here - it's handled by #rewrite_call
131
+ if anchor = style_attrs['text-anchor']
132
+ opts[:text_anchor] = anchor
133
+ end
134
+
135
+ calls << ['text_box', [element.text, opts], []]
136
+
137
+ when 'line'
138
+ calls << ['line', [x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])], []]
139
+
140
+ when 'polyline'
141
+ points = attrs['points'].split(/\s+/)
142
+ x, y = points.shift.split(",")
143
+ calls << ['move_to', [x(x), y(y)], []]
144
+ calls << ['stroke', [], []]
145
+ calls = calls.last.last
146
+ points.each do |point|
147
+ x, y = point.split(",")
148
+ calls << ["line_to", [x(x), y(y)], []]
149
+ end
150
+
151
+ when 'polygon'
152
+ points = attrs['points'].split(/\s+/).collect do |point|
153
+ x, y = point.split(",")
154
+ [x(x), y(y)]
155
+ end
156
+ calls << ["polygon", points, []]
157
+
158
+ when 'circle'
159
+ calls << ["circle_at",
160
+ [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}],
161
+ []]
162
+
163
+ when 'ellipse'
164
+ calls << ["ellipse_at",
165
+ [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])],
166
+ []]
167
+
168
+ when 'rect'
169
+ radius = distance(attrs['rx'] || attrs['ry'])
170
+ args = [[x(attrs['x']), y(attrs['y'])], distance(attrs['width']), distance(attrs['height'])]
171
+ if radius
172
+ # n.b. does not support both rx and ry being specified with different values
173
+ calls << ["rounded_rectangle", args + [radius], []]
174
+ else
175
+ calls << ["rectangle", args, []]
176
+ end
177
+
178
+ when 'path'
179
+ @svg_path ||= Path.new
180
+ @svg_path.parse(attrs['d']).each do |command, args|
181
+ point_to = [x(args[0]), y(args[1])]
182
+ if command == 'curve_to'
183
+ bounds = [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]
184
+ calls << [command, [point_to, {:bounds => bounds}], []]
185
+ else
186
+ calls << [command, point_to, []]
187
+ end
188
+ end
189
+
190
+ else
191
+ #raise "unknown tag #{element.name}"
192
+ end
193
+ end
194
+
195
+ def parse_css_declarations(declarations)
196
+ # copied from css_parser
197
+ declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')
198
+
199
+ {}.tap do |o|
200
+ declarations.split(/[\;$]+/m).each do |decs|
201
+ if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
202
+ property, value, end_of_declaration = matches.captures
203
+ o[property] = value
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ def apply_styles(attrs, calls, state)
210
+ draw_types = []
211
+
212
+ decs = attrs["style"] ? parse_css_declarations(attrs["style"]) : {}
213
+ attrs.each {|n,v| decs[n] = v unless decs[n]}
214
+
215
+ # Opacity:
216
+ # We can't do nested opacities quite like the SVG requires, but this is close enough.
217
+ fill_opacity = stroke_opacity = clamp(decs['opacity'].to_f, 0, 1) if decs['opacity']
218
+ fill_opacity = clamp(decs['fill-opacity'].to_f, 0, 1) if decs['fill-opacity']
219
+ stroke_opacity = clamp(decs['stroke-opacity'].to_f, 0, 1) if decs['stroke-opacity']
220
+
221
+ if fill_opacity || stroke_opacity
222
+ state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1)
223
+ state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1)
224
+
225
+ calls << ['transparent', [state[:fill_opacity], state[:stroke_opacity]], []]
226
+ calls = calls.last.last
227
+ end
228
+
229
+ if decs['fill'] && decs['fill'] != "none"
230
+ if color = color_to_hex(decs['fill'])
231
+ calls << ['fill_color', [color], []]
232
+ end
233
+ draw_types << 'fill'
234
+ end
235
+
236
+ if decs['stroke'] && decs['stroke'] != "none"
237
+ if color = color_to_hex(decs['stroke'])
238
+ calls << ['stroke_color', [color], []]
239
+ end
240
+ draw_types << 'stroke'
241
+ end
242
+
243
+ calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width']
244
+
245
+ [calls, decs, draw_types.join("_and_")]
246
+ end
247
+
248
+ def parse_css_method_calls(string)
249
+ string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
250
+ name, argument_string = call
251
+ arguments = argument_string.split(",").collect(&:strip)
252
+ [name, arguments]
253
+ end
254
+ end
255
+
256
+ # TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords
257
+ HTML_COLORS = {
258
+ 'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00",
259
+ 'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00",
260
+ 'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff",
261
+ 'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff"
262
+ }.freeze
263
+
264
+ def color_to_hex(color_string)
265
+ color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_|
266
+ if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i)
267
+ break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}"
268
+ elsif color.match(/\A#[0-9a-f]{6}\z/i)
269
+ break color[1..6]
270
+ elsif hex = HTML_COLORS[color.downcase]
271
+ break hex
272
+ elsif m = color.match(/\Argb\(\s*(-?[0-9.]+%?)\s*,\s*(-?[0-9.]+%?)\s*,\s*(-?[0-9.]+%?)\s*\)\z/i)
273
+ break (1..3).collect do |n|
274
+ value = m[n].to_f
275
+ value *= 2.55 if m[n][-1..-1] == '%'
276
+ "%02x" % clamp(value.round, 0, 255)
277
+ end.join
278
+ end
279
+ end
280
+ end
281
+
282
+ def x(value)
283
+ (pixels(value) - @x_offset) * scale
284
+ end
285
+
286
+ def y(value)
287
+ (@actual_height - (pixels(value) - @y_offset)) * scale
288
+ end
289
+
290
+ def distance(value)
291
+ value && (pixels(value) * scale)
292
+ end
293
+
294
+ def pixels(value)
295
+ if value.is_a?(String) && match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/)
296
+ send("#{match[1]}2pt", value.to_f)
297
+ else
298
+ value.to_f
299
+ end
300
+ end
301
+
302
+ def calculate_dimensions
303
+ if @options[:width]
304
+ @width = @options[:width]
305
+ @scale = @options[:width] / @actual_width.to_f
306
+ elsif @options[:height]
307
+ @height = @options[:height]
308
+ @scale = @options[:height] / @actual_height.to_f
309
+ else
310
+ @scale = 1
311
+ end
312
+
313
+ @width ||= @actual_width * @scale
314
+ @height ||= @actual_height * @scale
315
+ end
316
+
317
+ def clamp(value, min_value, max_value)
318
+ [[value, min_value].max, max_value].min
319
+ end
320
+ end
@@ -0,0 +1,167 @@
1
+ class Prawn::Svg::Path
2
+ def parse(data)
3
+ cmd = values = value = nil
4
+ @subpath_initial_point = @last_point = nil
5
+ @previous_control_point = @previous_quadratic_control_point = nil
6
+ @calls = []
7
+
8
+ data.each_char do |c|
9
+ if c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'
10
+ run_path_command(cmd, values) if cmd
11
+ cmd = c
12
+ values = []
13
+ value = ""
14
+ elsif c >= '0' && c <= '9' || c == '.' || c == "-"
15
+ value << c
16
+ elsif c == ' ' || c == "\t" || c == "\r" || c == "\n" || c == ","
17
+ if value != ""
18
+ values << value.to_f
19
+ value = ""
20
+ end
21
+ else
22
+ raise "invalid character '#{c}' in path data"
23
+ end
24
+ end
25
+
26
+ values << value.to_f if value != ""
27
+ run_path_command(cmd, values) if cmd
28
+
29
+ @calls
30
+ end
31
+
32
+ def run_path_command(command, values)
33
+ upcase_command = command.upcase
34
+ relative = command != upcase_command
35
+
36
+ case upcase_command
37
+ when 'M' # moveto
38
+ x = values.shift
39
+ y = values.shift
40
+
41
+ if relative && @last_point
42
+ x += @last_point.first
43
+ y += @last_point.last
44
+ end
45
+
46
+ @last_point = @subpath_initial_point = [x, y]
47
+ @calls << ["move_to", @last_point]
48
+
49
+ return run_path_command('L', values) if values.any?
50
+
51
+ when 'Z' # closepath
52
+ if @subpath_initial_point
53
+ @calls << ["line_to", @subpath_initial_point]
54
+ @last_point = @subpath_initial_point
55
+ end
56
+
57
+ when 'L' # lineto
58
+ while values.any?
59
+ x = values.shift
60
+ y = values.shift
61
+ if relative && @last_point
62
+ x += @last_point.first
63
+ y += @last_point.last
64
+ end
65
+ @last_point = [x, y]
66
+ @calls << ["line_to", @last_point]
67
+ end
68
+
69
+ when 'H' # horizontal lineto
70
+ while values.any?
71
+ x = values.shift
72
+ x += @last_point.first if relative && @last_point
73
+ @last_point = [x, @last_point.last]
74
+ @calls << ["line_to", @last_point]
75
+ end
76
+
77
+ when 'V' # vertical lineto
78
+ while values.any?
79
+ y = values.shift
80
+ y += @last_point.last if relative && @last_point
81
+ @last_point = [@last_point.first, y]
82
+ @calls << ["line_to", @last_point]
83
+ end
84
+
85
+ when 'C' # curveto
86
+ while values.any?
87
+ x1, y1, x2, y2, x, y = (1..6).collect {values.shift}
88
+ if relative && @last_point
89
+ x += @last_point.first
90
+ x1 += @last_point.first
91
+ x2 += @last_point.first
92
+ y += @last_point.last
93
+ y1 += @last_point.last
94
+ y2 += @last_point.last
95
+ end
96
+
97
+ @last_point = [x, y]
98
+ @previous_control_point = [x2, y2]
99
+ @calls << ["curve_to", [x, y, x1, y1, x2, y2]]
100
+ end
101
+
102
+ when 'S' # shorthand/smooth curveto
103
+ while values.any?
104
+ x2, y2, x, y = (1..4).collect {values.shift}
105
+ if relative && @last_point
106
+ x += @last_point.first
107
+ x2 += @last_point.first
108
+ y += @last_point.last
109
+ y2 += @last_point.last
110
+ end
111
+
112
+ if @previous_control_point
113
+ x1 = 2 * @last_point.first - @previous_control_point.first
114
+ y1 = 2 * @last_point.last - @previous_control_point.last
115
+ else
116
+ x1, y1 = @last_point
117
+ end
118
+
119
+ @last_point = [x, y]
120
+ @previous_control_point = [x2, y2]
121
+ @calls << ["curve_to", [x, y, x1, y1, x2, y2]]
122
+ end
123
+
124
+ when 'Q', 'T' # quadratic curveto
125
+ while values.any?
126
+ if shorthand = upcase_command == 'T'
127
+ x, y = (1..2).collect {values.shift}
128
+ else
129
+ x1, y1, x, y = (1..4).collect {values.shift}
130
+ end
131
+
132
+ if relative && @last_point
133
+ x += @last_point.first
134
+ x1 += @last_point.first if x1
135
+ y += @last_point.last
136
+ y1 += @last_point.last if y1
137
+ end
138
+
139
+ if shorthand
140
+ if @previous_quadratic_control_point
141
+ x1 = 2 * @last_point.first - @previous_quadratic_control_point.first
142
+ y1 = 2 * @last_point.last - @previous_quadratic_control_point.last
143
+ else
144
+ x1, y1 = @last_point
145
+ end
146
+ end
147
+
148
+ # convert from quadratic to cubic
149
+ cx1 = @last_point.first + (x1 - @last_point.first) * 2 / 3.0
150
+ cy1 = @last_point.last + (y1 - @last_point.last) * 2 / 3.0
151
+ cx2 = cx1 + (x - @last_point.first) / 3.0
152
+ cy2 = cy1 + (y - @last_point.last) / 3.0
153
+
154
+ @last_point = [x, y]
155
+ @previous_quadratic_control_point = [x1, y1]
156
+
157
+ @calls << ["curve_to", [x, y, cx1, cy1, cx2, cy2]]
158
+ end
159
+
160
+ when 'A'
161
+ # unsupported
162
+ end
163
+
164
+ @previous_control_point = nil unless %w(C S).include?(upcase_command)
165
+ @previous_quadratic_control_point = nil unless %w(Q T).include?(upcase_command)
166
+ end
167
+ end
@@ -0,0 +1,7 @@
1
+ module Prawn
2
+ class Document
3
+ def svg(data, options={})
4
+ Prawn::Svg.new(data, self, options).draw
5
+ end
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prawn-svg
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 9
8
+ - 1
9
+ version: 0.9.1
10
+ platform: ruby
11
+ authors:
12
+ - Roger Nesbitt
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-26 00:00:00 +13:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: SVG renderer for Prawn PDF library
22
+ email: roger@seriousorange.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - README
31
+ - LICENSE
32
+ - lib/prawn/svg/svg.rb
33
+ - lib/prawn/svg/svg_path.rb
34
+ - lib/prawn/svg_document.rb
35
+ - lib/prawn-svg.rb
36
+ has_rdoc: true
37
+ homepage:
38
+ licenses: []
39
+
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ segments:
50
+ - 0
51
+ version: "0"
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.3.6
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: SVG renderer for Prawn PDF library
66
+ test_files: []
67
+