prawn-svg 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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
+