prawn-svg 0.9.1.6 → 0.9.1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/prawn-svg.rb +5 -3
- data/lib/prawn/svg/extension.rb +24 -0
- data/lib/prawn/svg/interface.rb +87 -0
- data/lib/prawn/svg/parser.rb +368 -362
- data/lib/prawn/svg/parser/path.rb +184 -0
- metadata +6 -6
- data/lib/prawn/svg/path.rb +0 -180
- data/lib/prawn/svg/svg.rb +0 -83
- data/lib/prawn/svg_document.rb +0 -22
data/lib/prawn-svg.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'prawn'
|
2
|
-
require 'prawn/
|
3
|
-
require 'prawn/svg/
|
2
|
+
require 'prawn/svg/extension'
|
3
|
+
require 'prawn/svg/interface'
|
4
4
|
require 'prawn/svg/parser'
|
5
|
-
require 'prawn/svg/path'
|
5
|
+
require 'prawn/svg/parser/path'
|
6
|
+
|
7
|
+
Prawn::Document.extensions << Prawn::Svg::Extension
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Prawn
|
2
|
+
module Svg
|
3
|
+
module Extension
|
4
|
+
#
|
5
|
+
# Draws an SVG document into the PDF.
|
6
|
+
#
|
7
|
+
# +options+ must contain the key :at, which takes a tuple of x and y co-ordinates.
|
8
|
+
#
|
9
|
+
# +options+ can optionally contain the key :width or :height. If both are
|
10
|
+
# specified, only :width will be used. If neither are specified, the resolution
|
11
|
+
# given in the SVG will be used.
|
12
|
+
#
|
13
|
+
# Example usage:
|
14
|
+
#
|
15
|
+
# svg IO.read("example.svg"), :at => [100, 300], :width => 600
|
16
|
+
#
|
17
|
+
def svg(data, options={})
|
18
|
+
svg = Prawn::Svg::Interface.new(data, self, options)
|
19
|
+
svg.draw
|
20
|
+
{:warnings => svg.parser_warnings, :width => svg.parser.width, :height => svg.parser.height}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
#
|
2
|
+
# Prawn::Svg::Interface makes a Prawn::Svg::Parser instance, uses that object to parse the supplied
|
3
|
+
# SVG into Prawn-compatible method calls, and then calls the Prawn methods.
|
4
|
+
#
|
5
|
+
module Prawn
|
6
|
+
module Svg
|
7
|
+
class Interface
|
8
|
+
DEFAULT_FONT_PATHS = ["/Library/Fonts", "/usr/share/fonts/truetype/**"]
|
9
|
+
|
10
|
+
@font_path = []
|
11
|
+
DEFAULT_FONT_PATHS.each {|path| @font_path << path if File.exists?(path)}
|
12
|
+
|
13
|
+
class << self; attr_accessor :font_path; end
|
14
|
+
|
15
|
+
attr_reader :data, :prawn, :parser, :options
|
16
|
+
|
17
|
+
# An +Array+ of warnings that occurred while parsing the SVG data. If this array is non-empty,
|
18
|
+
# it's likely that the SVG failed to render correctly.
|
19
|
+
attr_reader :parser_warnings
|
20
|
+
|
21
|
+
#
|
22
|
+
# Creates a Prawn::Svg object.
|
23
|
+
#
|
24
|
+
# +data+ is the SVG data to convert. +prawn+ is your Prawn::Document object.
|
25
|
+
#
|
26
|
+
# +options+ must contain the key :at, which takes a tuple of x and y co-ordinates.
|
27
|
+
#
|
28
|
+
# +options+ can optionally contain the key :width or :height. If both are
|
29
|
+
# specified, only :width will be used.
|
30
|
+
#
|
31
|
+
def initialize(data, prawn, options)
|
32
|
+
@data = data
|
33
|
+
@prawn = prawn
|
34
|
+
@options = options
|
35
|
+
|
36
|
+
@options[:at] or raise "options[:at] must be specified"
|
37
|
+
|
38
|
+
@parser = Parser.new(data, [prawn.bounds.width, prawn.bounds.height], options)
|
39
|
+
@parser_warnings = @parser.warnings
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Draws the SVG to the Prawn::Document object.
|
44
|
+
#
|
45
|
+
def draw
|
46
|
+
prawn.bounding_box(@options[:at], :width => @parser.width, :height => @parser.height) do
|
47
|
+
prawn.save_graphics_state do
|
48
|
+
proc_creator(prawn, @parser.parse).call
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
private
|
55
|
+
def proc_creator(prawn, calls)
|
56
|
+
Proc.new {issue_prawn_command(prawn, calls)}
|
57
|
+
end
|
58
|
+
|
59
|
+
def issue_prawn_command(prawn, calls)
|
60
|
+
calls.each do |call, arguments, children|
|
61
|
+
if rewrite_call_arguments(prawn, call, arguments) == false
|
62
|
+
issue_prawn_command(prawn, children) if children.any?
|
63
|
+
else
|
64
|
+
if children.empty?
|
65
|
+
prawn.send(call, *arguments)
|
66
|
+
else
|
67
|
+
prawn.send(call, *arguments, &proc_creator(prawn, children))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def rewrite_call_arguments(prawn, call, arguments)
|
74
|
+
case call
|
75
|
+
when 'text_box'
|
76
|
+
if (anchor = arguments.last.delete(:text_anchor)) && %w(middle end).include?(anchor)
|
77
|
+
width = prawn.width_of(*arguments)
|
78
|
+
width /= 2 if anchor == 'middle'
|
79
|
+
arguments.last[:at][0] -= width
|
80
|
+
end
|
81
|
+
|
82
|
+
arguments.last[:at][1] += prawn.height_of(*arguments) / 3 * 2
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/prawn/svg/parser.rb
CHANGED
@@ -11,441 +11,447 @@ require 'rexml/document'
|
|
11
11
|
# prawn specifically - this might be useful if you want to take this code and use it to convert
|
12
12
|
# SVG to another format.
|
13
13
|
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
14
|
+
module Prawn
|
15
|
+
module Svg
|
16
|
+
class Parser
|
17
|
+
begin
|
18
|
+
require 'css_parser'
|
19
|
+
CSS_PARSER_LOADED = true
|
20
|
+
rescue LoadError
|
21
|
+
CSS_PARSER_LOADED = false
|
22
|
+
end
|
21
23
|
|
22
|
-
|
24
|
+
include Prawn::Measurements
|
23
25
|
|
24
|
-
|
26
|
+
attr_reader :width, :height
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
+
# An +Array+ of warnings that occurred while parsing the SVG data.
|
29
|
+
attr_reader :warnings
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
+
# The scaling factor, as determined by the :width or :height options.
|
32
|
+
attr_accessor :scale
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
34
|
+
#
|
35
|
+
# Construct a Parser object.
|
36
|
+
#
|
37
|
+
# The +data+ argument is SVG data.
|
38
|
+
#
|
39
|
+
# +bounds+ is a tuple [width, height] that specifies the bounds of the drawing space in points.
|
40
|
+
#
|
41
|
+
# +options+ can optionally contain
|
42
|
+
# the key :width or :height. If both are specified, only :width will be used.
|
43
|
+
#
|
44
|
+
def initialize(data, bounds, options)
|
45
|
+
@data = data
|
46
|
+
@bounds = bounds
|
47
|
+
@options = options
|
48
|
+
@warnings = []
|
49
|
+
@css_parser = CssParser::Parser.new if CSS_PARSER_LOADED
|
48
50
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
51
|
+
if data
|
52
|
+
parse_document
|
53
|
+
calculate_dimensions
|
54
|
+
end
|
55
|
+
end
|
54
56
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
57
|
+
#
|
58
|
+
# Parse the SVG data and return a call tree. The returned +Array+ is in the format:
|
59
|
+
#
|
60
|
+
# [
|
61
|
+
# ['prawn_method_name', ['argument1', 'argument2'], []],
|
62
|
+
# ['method_that_takes_a_block', ['argument1', 'argument2'], [
|
63
|
+
# ['method_called_inside_block', ['argument'], []]
|
64
|
+
# ]
|
65
|
+
# ]
|
66
|
+
#
|
67
|
+
def parse
|
68
|
+
@warnings = []
|
69
|
+
calls = []
|
70
|
+
parse_element(@root, calls, {})
|
71
|
+
calls
|
72
|
+
end
|
69
73
|
|
70
74
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
+
private
|
76
|
+
def parse_document
|
77
|
+
@root = REXML::Document.new(@data).root
|
78
|
+
@actual_width, @actual_height = @bounds # set this first so % width/heights can be used
|
75
79
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
80
|
+
if vb = @root.attributes['viewBox']
|
81
|
+
x1, y1, x2, y2 = vb.strip.split(/\s+/)
|
82
|
+
@x_offset, @y_offset = [x1.to_f, y1.to_f]
|
83
|
+
@actual_width, @actual_height = [x2.to_f - x1.to_f, y2.to_f - y1.to_f]
|
84
|
+
else
|
85
|
+
@x_offset, @y_offset = [0, 0]
|
86
|
+
@actual_width = points(@root.attributes['width'], :x)
|
87
|
+
@actual_height = points(@root.attributes['height'], :y)
|
88
|
+
end
|
89
|
+
end
|
86
90
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
91
|
+
REQUIRED_ATTRIBUTES = {
|
92
|
+
"line" => %w(x1 y1 x2 y2),
|
93
|
+
"polyline" => %w(points),
|
94
|
+
"polygon" => %w(points),
|
95
|
+
"circle" => %w(r),
|
96
|
+
"ellipse" => %w(rx ry),
|
97
|
+
"rect" => %w(x y width height),
|
98
|
+
"path" => %w(d)
|
99
|
+
}
|
96
100
|
|
97
|
-
|
98
|
-
|
99
|
-
|
101
|
+
def parse_element(element, calls, state)
|
102
|
+
attrs = element.attributes
|
103
|
+
calls, style_attrs = apply_styles(element, calls, state)
|
100
104
|
|
101
|
-
|
102
|
-
|
103
|
-
|
105
|
+
if required_attributes = REQUIRED_ATTRIBUTES[element.name]
|
106
|
+
return unless check_attrs_present(element, required_attributes)
|
107
|
+
end
|
104
108
|
|
105
|
-
|
106
|
-
|
107
|
-
|
109
|
+
case element.name
|
110
|
+
when 'title', 'desc'
|
111
|
+
# ignore
|
108
112
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
+
when 'g', 'svg'
|
114
|
+
element.elements.each do |child|
|
115
|
+
parse_element(child, calls, state.dup)
|
116
|
+
end
|
113
117
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
118
|
+
when 'defs'
|
119
|
+
# Pass calls as a blank array so that nothing under this tag can be added to our call tree.
|
120
|
+
element.elements.each do |child|
|
121
|
+
parse_element(child, [], state.dup.merge(:display => false))
|
122
|
+
end
|
119
123
|
|
120
|
-
|
121
|
-
|
124
|
+
when 'style'
|
125
|
+
load_css_styles(element)
|
122
126
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
127
|
+
when 'text'
|
128
|
+
# Very primitive support for font-family; it won't work in most cases because
|
129
|
+
# PDF only has a few built-in fonts, and they're not the same as the names
|
130
|
+
# used typically with the web fonts.
|
131
|
+
if font_family = style_attrs["font-family"]
|
132
|
+
if font_family != "" && pdf_font = map_font_family_to_pdf_font(font_family)
|
133
|
+
calls << ['font', [pdf_font], []]
|
134
|
+
calls = calls.last.last
|
135
|
+
else
|
136
|
+
@warnings << "#{font_family} is not a known font."
|
137
|
+
end
|
138
|
+
end
|
135
139
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
+
opts = {:at => [x(attrs['x']), y(attrs['y'])]}
|
141
|
+
if size = style_attrs['font-size']
|
142
|
+
opts[:size] = size.to_f * @scale
|
143
|
+
end
|
140
144
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
145
|
+
# This is not a prawn option but we can't work out how to render it here -
|
146
|
+
# it's handled by Svg#rewrite_call_arguments
|
147
|
+
if anchor = style_attrs['text-anchor']
|
148
|
+
opts[:text_anchor] = anchor
|
149
|
+
end
|
146
150
|
|
147
|
-
|
151
|
+
calls << ['text_box', [element.text, opts], []]
|
148
152
|
|
149
|
-
|
150
|
-
|
153
|
+
when 'line'
|
154
|
+
calls << ['line', [x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])], []]
|
151
155
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
156
|
+
when 'polyline'
|
157
|
+
points = attrs['points'].split(/\s+/)
|
158
|
+
return unless base_point = points.shift
|
159
|
+
x, y = base_point.split(",")
|
160
|
+
calls << ['move_to', [x(x), y(y)], []]
|
161
|
+
calls << ['stroke', [], []]
|
162
|
+
calls = calls.last.last
|
163
|
+
points.each do |point|
|
164
|
+
x, y = point.split(",")
|
165
|
+
calls << ["line_to", [x(x), y(y)], []]
|
166
|
+
end
|
163
167
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
168
|
+
when 'polygon'
|
169
|
+
points = attrs['points'].split(/\s+/).collect do |point|
|
170
|
+
x, y = point.split(",")
|
171
|
+
[x(x), y(y)]
|
172
|
+
end
|
173
|
+
calls << ["polygon", points, []]
|
170
174
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
+
when 'circle'
|
176
|
+
calls << ["circle_at",
|
177
|
+
[[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}],
|
178
|
+
[]]
|
175
179
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
+
when 'ellipse'
|
181
|
+
calls << ["ellipse_at",
|
182
|
+
[[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])],
|
183
|
+
[]]
|
180
184
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
185
|
+
when 'rect'
|
186
|
+
radius = distance(attrs['rx'] || attrs['ry'])
|
187
|
+
args = [[x(attrs['x']), y(attrs['y'])], distance(attrs['width']), distance(attrs['height'])]
|
188
|
+
if radius
|
189
|
+
# n.b. does not support both rx and ry being specified with different values
|
190
|
+
calls << ["rounded_rectangle", args + [radius], []]
|
191
|
+
else
|
192
|
+
calls << ["rectangle", args, []]
|
193
|
+
end
|
190
194
|
|
191
|
-
|
192
|
-
|
195
|
+
when 'path'
|
196
|
+
@svg_path ||= Path.new
|
193
197
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
198
|
+
begin
|
199
|
+
commands = @svg_path.parse(attrs['d'])
|
200
|
+
rescue Prawn::Svg::Parser::Path::InvalidError => e
|
201
|
+
commands = []
|
202
|
+
@warnings << e.message
|
203
|
+
end
|
200
204
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
205
|
+
commands.each do |command, args|
|
206
|
+
point_to = [x(args[0]), y(args[1])]
|
207
|
+
if command == 'curve_to'
|
208
|
+
bounds = [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]
|
209
|
+
calls << [command, [point_to, {:bounds => bounds}], []]
|
210
|
+
else
|
211
|
+
calls << [command, point_to, []]
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
else
|
216
|
+
@warnings << "Unknown tag '#{element.name}'; ignoring"
|
208
217
|
end
|
209
218
|
end
|
210
|
-
|
211
|
-
else
|
212
|
-
@warnings << "Unknown tag '#{element.name}'; ignoring"
|
213
|
-
end
|
214
|
-
end
|
215
219
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
220
|
+
def load_css_styles(element)
|
221
|
+
if @css_parser
|
222
|
+
data = if element.cdatas.any?
|
223
|
+
element.cdatas.collect(&:to_s).join
|
224
|
+
else
|
225
|
+
element.text
|
226
|
+
end
|
223
227
|
|
224
|
-
|
225
|
-
|
226
|
-
|
228
|
+
@css_parser.add_block!(data)
|
229
|
+
end
|
230
|
+
end
|
227
231
|
|
228
|
-
|
229
|
-
|
230
|
-
|
232
|
+
def parse_css_declarations(declarations)
|
233
|
+
# copied from css_parser
|
234
|
+
declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')
|
231
235
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
236
|
+
output = {}
|
237
|
+
declarations.split(/[\;$]+/m).each do |decs|
|
238
|
+
if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
|
239
|
+
property, value, end_of_declaration = matches.captures
|
240
|
+
output[property] = value
|
241
|
+
end
|
237
242
|
end
|
243
|
+
output
|
238
244
|
end
|
239
|
-
end
|
240
|
-
end
|
241
245
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
+
def determine_style_for(element)
|
247
|
+
if @css_parser
|
248
|
+
tag_style = @css_parser.find_by_selector(element.name)
|
249
|
+
id_style = @css_parser.find_by_selector("##{element.attributes["id"]}") if element.attributes["id"]
|
246
250
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
251
|
+
if classes = element.attributes["class"]
|
252
|
+
class_styles = classes.strip.split(/\s+/).collect do |class_name|
|
253
|
+
@css_parser.find_by_selector(".#{class_name}")
|
254
|
+
end
|
255
|
+
end
|
252
256
|
|
253
|
-
|
257
|
+
element_style = element.attributes['style']
|
254
258
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
else
|
259
|
-
style = element.attributes['style'] || ""
|
260
|
-
end
|
261
|
-
|
262
|
-
decs = parse_css_declarations(style)
|
263
|
-
element.attributes.each {|n,v| decs[n] = v unless decs[n]}
|
264
|
-
decs
|
265
|
-
end
|
266
|
-
|
267
|
-
def apply_styles(element, calls, state)
|
268
|
-
decs = determine_style_for(element)
|
269
|
-
draw_types = []
|
270
|
-
|
271
|
-
# Transform
|
272
|
-
if transform = decs['transform']
|
273
|
-
parse_css_method_calls(transform).each do |name, arguments|
|
274
|
-
case name
|
275
|
-
when 'translate'
|
276
|
-
x, y = arguments
|
277
|
-
x, y = x.split(/\s+/) if y.nil?
|
278
|
-
calls << [name, [distance(x), -distance(y)], []]
|
279
|
-
calls = calls.last.last
|
280
|
-
when 'rotate'
|
281
|
-
calls << [name, [-arguments.first.to_f, {:origin => [0, y('0')]}], []]
|
282
|
-
calls = calls.last.last
|
283
|
-
when 'scale'
|
284
|
-
calls << [name, [arguments.first.to_f], []]
|
285
|
-
calls = calls.last.last
|
259
|
+
style = [tag_style, class_styles, id_style, element_style].flatten.collect do |s|
|
260
|
+
s.nil? || s.strip == "" ? "" : "#{s}#{";" unless s.match(/;\s*\z/)}"
|
261
|
+
end.join
|
286
262
|
else
|
287
|
-
|
263
|
+
style = element.attributes['style'] || ""
|
288
264
|
end
|
265
|
+
|
266
|
+
decs = parse_css_declarations(style)
|
267
|
+
element.attributes.each {|n,v| decs[n] = v unless decs[n]}
|
268
|
+
decs
|
289
269
|
end
|
290
|
-
|
270
|
+
|
271
|
+
def apply_styles(element, calls, state)
|
272
|
+
decs = determine_style_for(element)
|
273
|
+
draw_types = []
|
274
|
+
|
275
|
+
# Transform
|
276
|
+
if transform = decs['transform']
|
277
|
+
parse_css_method_calls(transform).each do |name, arguments|
|
278
|
+
case name
|
279
|
+
when 'translate'
|
280
|
+
x, y = arguments
|
281
|
+
x, y = x.split(/\s+/) if y.nil?
|
282
|
+
calls << [name, [distance(x), -distance(y)], []]
|
283
|
+
calls = calls.last.last
|
284
|
+
when 'rotate'
|
285
|
+
calls << [name, [-arguments.first.to_f, {:origin => [0, y('0')]}], []]
|
286
|
+
calls = calls.last.last
|
287
|
+
when 'scale'
|
288
|
+
calls << [name, [arguments.first.to_f], []]
|
289
|
+
calls = calls.last.last
|
290
|
+
else
|
291
|
+
@warnings << "Unknown transformation '#{name}'; ignoring"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
291
295
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
296
|
+
# Opacity:
|
297
|
+
# We can't do nested opacities quite like the SVG requires, but this is close enough.
|
298
|
+
fill_opacity = stroke_opacity = clamp(decs['opacity'].to_f, 0, 1) if decs['opacity']
|
299
|
+
fill_opacity = clamp(decs['fill-opacity'].to_f, 0, 1) if decs['fill-opacity']
|
300
|
+
stroke_opacity = clamp(decs['stroke-opacity'].to_f, 0, 1) if decs['stroke-opacity']
|
297
301
|
|
298
|
-
|
299
|
-
|
300
|
-
|
302
|
+
if fill_opacity || stroke_opacity
|
303
|
+
state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1)
|
304
|
+
state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1)
|
301
305
|
|
302
|
-
|
303
|
-
|
304
|
-
|
306
|
+
calls << ['transparent', [state[:fill_opacity], state[:stroke_opacity]], []]
|
307
|
+
calls = calls.last.last
|
308
|
+
end
|
305
309
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
310
|
+
# Fill and stroke
|
311
|
+
if decs['fill'] && decs['fill'] != "none"
|
312
|
+
if color = color_to_hex(decs['fill'])
|
313
|
+
calls << ['fill_color', [color], []]
|
314
|
+
end
|
315
|
+
draw_types << 'fill'
|
316
|
+
end
|
313
317
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
318
|
+
if decs['stroke'] && decs['stroke'] != "none"
|
319
|
+
if color = color_to_hex(decs['stroke'])
|
320
|
+
calls << ['stroke_color', [color], []]
|
321
|
+
end
|
322
|
+
draw_types << 'stroke'
|
323
|
+
end
|
320
324
|
|
321
|
-
|
325
|
+
calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width']
|
322
326
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
327
|
+
draw_type = draw_types.join("_and_")
|
328
|
+
state[:draw_type] = draw_type if draw_type != ""
|
329
|
+
if state[:draw_type] && !%w(g svg).include?(element.name)
|
330
|
+
calls << [state[:draw_type], [], []]
|
331
|
+
calls = calls.last.last
|
332
|
+
end
|
329
333
|
|
330
|
-
|
331
|
-
|
334
|
+
[calls, decs]
|
335
|
+
end
|
332
336
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
337
|
+
def parse_css_method_calls(string)
|
338
|
+
string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
|
339
|
+
name, argument_string = call
|
340
|
+
arguments = argument_string.split(",").collect(&:strip)
|
341
|
+
[name, arguments]
|
342
|
+
end
|
343
|
+
end
|
340
344
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
345
|
+
BUILT_IN_FONTS = ["Courier", "Helvetica", "Times-Roman", "Symbol", "ZapfDingbats"]
|
346
|
+
GENERIC_CSS_FONT_MAPPING = {
|
347
|
+
"serif" => "Times-Roman",
|
348
|
+
"sans-serif" => "Helvetica",
|
349
|
+
"cursive" => "Times-Roman",
|
350
|
+
"fantasy" => "Times-Roman",
|
351
|
+
"monospace" => "Courier"}
|
348
352
|
|
349
|
-
|
350
|
-
|
351
|
-
|
353
|
+
def installed_fonts
|
354
|
+
@installed_fonts ||= Prawn::Svg::Interface.font_path.uniq.collect {|path| Dir["#{path}/*"]}.flatten
|
355
|
+
end
|
352
356
|
|
353
|
-
|
354
|
-
|
355
|
-
|
357
|
+
def map_font_family_to_pdf_font(font_family)
|
358
|
+
font_family.split(",").detect do |font|
|
359
|
+
font = font.gsub(/['"]/, '').gsub(/\s{2,}/, ' ').strip.downcase
|
356
360
|
|
357
|
-
|
358
|
-
|
361
|
+
built_in_font = BUILT_IN_FONTS.detect {|f| f.downcase == font}
|
362
|
+
break built_in_font if built_in_font
|
359
363
|
|
360
|
-
|
361
|
-
|
364
|
+
generic_font = GENERIC_CSS_FONT_MAPPING[font]
|
365
|
+
break generic_font if generic_font
|
362
366
|
|
363
|
-
|
364
|
-
|
367
|
+
installed_font = installed_fonts.detect do |file|
|
368
|
+
(matches = File.basename(file).match(/(.+)\./)) && matches[1].downcase == font
|
369
|
+
end
|
370
|
+
break installed_font if installed_font
|
371
|
+
end
|
365
372
|
end
|
366
|
-
break installed_font if installed_font
|
367
|
-
end
|
368
|
-
end
|
369
373
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
374
|
+
# TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords
|
375
|
+
HTML_COLORS = {
|
376
|
+
'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00",
|
377
|
+
'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00",
|
378
|
+
'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff",
|
379
|
+
'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff"
|
380
|
+
}.freeze
|
377
381
|
|
378
|
-
|
379
|
-
|
382
|
+
RGB_VALUE_REGEXP = "\s*(-?[0-9.]+%?)\s*"
|
383
|
+
RGB_REGEXP = /\Argb\(#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP}\)\z/i
|
380
384
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
385
|
+
def color_to_hex(color_string)
|
386
|
+
color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_|
|
387
|
+
if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i)
|
388
|
+
break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}"
|
389
|
+
elsif color.match(/\A#[0-9a-f]{6}\z/i)
|
390
|
+
break color[1..6]
|
391
|
+
elsif hex = HTML_COLORS[color.downcase]
|
392
|
+
break hex
|
393
|
+
elsif m = color.match(RGB_REGEXP)
|
394
|
+
break (1..3).collect do |n|
|
395
|
+
value = m[n].to_f
|
396
|
+
value *= 2.55 if m[n][-1..-1] == '%'
|
397
|
+
"%02x" % clamp(value.round, 0, 255)
|
398
|
+
end.join
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
398
402
|
|
399
|
-
|
400
|
-
|
401
|
-
|
403
|
+
def x(value)
|
404
|
+
(points(value, :x) - @x_offset) * scale
|
405
|
+
end
|
402
406
|
|
403
|
-
|
404
|
-
|
405
|
-
|
407
|
+
def y(value)
|
408
|
+
(@actual_height - (points(value, :y) - @y_offset)) * scale
|
409
|
+
end
|
406
410
|
|
407
|
-
|
408
|
-
|
409
|
-
|
411
|
+
def distance(value, axis = nil)
|
412
|
+
value && (points(value, axis) * scale)
|
413
|
+
end
|
410
414
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
415
|
+
def points(value, axis = nil)
|
416
|
+
if value.is_a?(String)
|
417
|
+
if match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/)
|
418
|
+
send("#{match[1]}2pt", value.to_f)
|
419
|
+
elsif value[-1..-1] == "%"
|
420
|
+
value.to_f * (axis == :y ? @actual_height : @actual_width) / 100.0
|
421
|
+
else
|
422
|
+
value.to_f
|
423
|
+
end
|
424
|
+
else
|
425
|
+
value.to_f
|
426
|
+
end
|
419
427
|
end
|
420
|
-
else
|
421
|
-
value.to_f
|
422
|
-
end
|
423
|
-
end
|
424
428
|
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
429
|
+
def calculate_dimensions
|
430
|
+
if @options[:width]
|
431
|
+
@width = @options[:width]
|
432
|
+
@scale = @options[:width] / @actual_width.to_f
|
433
|
+
elsif @options[:height]
|
434
|
+
@height = @options[:height]
|
435
|
+
@scale = @options[:height] / @actual_height.to_f
|
436
|
+
else
|
437
|
+
@scale = 1
|
438
|
+
end
|
435
439
|
|
436
|
-
|
437
|
-
|
438
|
-
|
440
|
+
@width ||= @actual_width * @scale
|
441
|
+
@height ||= @actual_height * @scale
|
442
|
+
end
|
439
443
|
|
440
|
-
|
441
|
-
|
442
|
-
|
444
|
+
def clamp(value, min_value, max_value)
|
445
|
+
[[value, min_value].max, max_value].min
|
446
|
+
end
|
443
447
|
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
+
def check_attrs_present(element, attrs)
|
449
|
+
missing_attrs = attrs - element.attributes.keys
|
450
|
+
if missing_attrs.any?
|
451
|
+
@warnings << "Must have attributes #{missing_attrs.join(", ")} on tag #{element.name}; skipping tag"
|
452
|
+
end
|
453
|
+
missing_attrs.empty?
|
454
|
+
end
|
448
455
|
end
|
449
|
-
missing_attrs.empty?
|
450
456
|
end
|
451
457
|
end
|