prawn-svg 0.9.1.8 → 0.9.1.9
Sign up to get free protection for your applications and to get access to all the features.
- data/README +8 -5
- data/lib/prawn-svg.rb +4 -0
- data/lib/prawn/svg/document.rb +81 -0
- data/lib/prawn/svg/element.rb +220 -0
- data/lib/prawn/svg/extension.rb +1 -1
- data/lib/prawn/svg/font.rb +100 -0
- data/lib/prawn/svg/interface.rb +38 -16
- data/lib/prawn/svg/parser.rb +175 -417
- data/lib/prawn/svg/parser/text.rb +67 -0
- metadata +7 -3
data/README
CHANGED
@@ -28,15 +28,18 @@ prawn-svg is in its infancy and does not support the full SVG specifications. I
|
|
28
28
|
supports moveto, closepath, lineto, horiz lineto, vert lineto, curveto, smooth curveto, quad curveto, smooth quad curveto
|
29
29
|
does not support elliptical arc
|
30
30
|
|
31
|
-
- text
|
32
|
-
attributes: size, text-anchor
|
33
|
-
|
31
|
+
- text and tspan tags
|
32
|
+
attributes: size, text-anchor, font-family, font-weight, dx, dy
|
33
|
+
|
34
|
+
- svg, g and symbol tags
|
35
|
+
|
36
|
+
- use tag
|
34
37
|
|
35
38
|
- style tag, if css_parser gem is installed on the system [1]
|
36
39
|
|
37
40
|
- attributes/styles: fill, stroke, stroke-width, opacity, fill-opacity, stroke-opacity, transform
|
38
41
|
|
39
|
-
- transform methods: translate, rotate, scale
|
42
|
+
- transform methods: translate, rotate, scale, matrix
|
40
43
|
|
41
44
|
- colors: html standard names, #xxx, #xxxxxx, rgb(1, 2, 3), rgb(1%, 2%, 3%)
|
42
45
|
|
@@ -49,6 +52,6 @@ Mac OS X and Debian Linux users. You can add to the font path:
|
|
49
52
|
|
50
53
|
Prawn::Svg.font_path << "/my/font/directory"
|
51
54
|
|
52
|
-
prawn-svg does NOT support
|
55
|
+
prawn-svg does NOT support external references, gradients/patterns or markers.
|
53
56
|
|
54
57
|
[1] If the css_parser gem is installed, it will handle CSS style definitions, but only simple tag, class or id definitions.
|
data/lib/prawn-svg.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
require 'prawn'
|
2
2
|
require 'prawn/svg/extension'
|
3
3
|
require 'prawn/svg/interface'
|
4
|
+
require 'prawn/svg/font'
|
5
|
+
require 'prawn/svg/document'
|
6
|
+
require 'prawn/svg/element'
|
4
7
|
require 'prawn/svg/parser'
|
5
8
|
require 'prawn/svg/parser/path'
|
9
|
+
require 'prawn/svg/parser/text'
|
6
10
|
|
7
11
|
Prawn::Document.extensions << Prawn::Svg::Extension
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class Prawn::Svg::Document
|
2
|
+
include Prawn::Measurements
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'css_parser'
|
6
|
+
CSS_PARSER_LOADED = true
|
7
|
+
rescue LoadError
|
8
|
+
CSS_PARSER_LOADED = false
|
9
|
+
end
|
10
|
+
|
11
|
+
DEFAULT_WIDTH = 640
|
12
|
+
DEFAULT_HEIGHT = 480
|
13
|
+
|
14
|
+
# An +Array+ of warnings that occurred while parsing the SVG data.
|
15
|
+
attr_reader :warnings
|
16
|
+
|
17
|
+
# The scaling factor, as determined by the :width or :height options.
|
18
|
+
attr_accessor :scale
|
19
|
+
|
20
|
+
attr_reader :root,
|
21
|
+
:actual_width, :actual_height, :width, :height, :x_offset, :y_offset,
|
22
|
+
:css_parser
|
23
|
+
|
24
|
+
def initialize(data, bounds, options)
|
25
|
+
@css_parser = CssParser::Parser.new if CSS_PARSER_LOADED
|
26
|
+
|
27
|
+
@root = REXML::Document.new(data).root
|
28
|
+
@warnings = []
|
29
|
+
@options = options
|
30
|
+
@actual_width, @actual_height = bounds # set this first so % width/heights can be used
|
31
|
+
|
32
|
+
if vb = @root.attributes['viewBox']
|
33
|
+
x1, y1, x2, y2 = vb.strip.split(/\s+/)
|
34
|
+
@x_offset, @y_offset = [x1.to_f, y1.to_f]
|
35
|
+
@actual_width, @actual_height = [x2.to_f - x1.to_f, y2.to_f - y1.to_f]
|
36
|
+
else
|
37
|
+
@x_offset, @y_offset = [0, 0]
|
38
|
+
@actual_width = points(@root.attributes['width'] || DEFAULT_WIDTH, :x)
|
39
|
+
@actual_height = points(@root.attributes['height'] || DEFAULT_HEIGHT, :y)
|
40
|
+
end
|
41
|
+
|
42
|
+
if @options[:width]
|
43
|
+
@width = @options[:width]
|
44
|
+
@scale = @options[:width] / @actual_width.to_f
|
45
|
+
elsif @options[:height]
|
46
|
+
@height = @options[:height]
|
47
|
+
@scale = @options[:height] / @actual_height.to_f
|
48
|
+
else
|
49
|
+
@scale = 1
|
50
|
+
end
|
51
|
+
|
52
|
+
@width ||= @actual_width * @scale
|
53
|
+
@height ||= @actual_height * @scale
|
54
|
+
end
|
55
|
+
|
56
|
+
def x(value)
|
57
|
+
(points(value, :x) - @x_offset) * scale
|
58
|
+
end
|
59
|
+
|
60
|
+
def y(value)
|
61
|
+
(@actual_height - (points(value, :y) - @y_offset)) * scale
|
62
|
+
end
|
63
|
+
|
64
|
+
def distance(value, axis = nil)
|
65
|
+
value && (points(value, axis) * scale)
|
66
|
+
end
|
67
|
+
|
68
|
+
def points(value, axis = nil)
|
69
|
+
if value.is_a?(String)
|
70
|
+
if match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/)
|
71
|
+
send("#{match[1]}2pt", value.to_f)
|
72
|
+
elsif value[-1..-1] == "%"
|
73
|
+
value.to_f * (axis == :y ? @actual_height : @actual_width) / 100.0
|
74
|
+
else
|
75
|
+
value.to_f
|
76
|
+
end
|
77
|
+
else
|
78
|
+
value.to_f
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
class Prawn::Svg::Element
|
2
|
+
attr_reader :document, :element, :parent_calls, :base_calls, :state, :attributes
|
3
|
+
attr_accessor :calls
|
4
|
+
|
5
|
+
def initialize(document, element, parent_calls, state)
|
6
|
+
@document = document
|
7
|
+
@element = element
|
8
|
+
@parent_calls = parent_calls
|
9
|
+
@state = state
|
10
|
+
@base_calls = @calls = []
|
11
|
+
|
12
|
+
combine_attributes_and_style_declarations
|
13
|
+
apply_styles
|
14
|
+
|
15
|
+
if id = @attributes["id"]
|
16
|
+
state[:ids][id] = @base_calls
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
@name ||= element.name
|
22
|
+
end
|
23
|
+
|
24
|
+
def each_child_element
|
25
|
+
element.elements.each do |e|
|
26
|
+
yield self.class.new(@document, e, @calls, @state.dup)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def warnings
|
31
|
+
@document.warnings
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_call(name, *arguments)
|
35
|
+
@calls << [name.to_s, arguments, []]
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_call_and_enter(name, *arguments)
|
39
|
+
@calls << [name.to_s, arguments, []]
|
40
|
+
@calls = @calls.last.last
|
41
|
+
end
|
42
|
+
|
43
|
+
def append_calls_to_parent
|
44
|
+
@parent_calls.concat(@base_calls)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
protected
|
49
|
+
def apply_styles
|
50
|
+
# Transform
|
51
|
+
if transform = @attributes['transform']
|
52
|
+
parse_css_method_calls(transform).each do |name, arguments|
|
53
|
+
case name
|
54
|
+
when 'translate'
|
55
|
+
x, y = arguments
|
56
|
+
x, y = x.split(/\s+/) if y.nil?
|
57
|
+
add_call_and_enter name, @document.distance(x), -@document.distance(y)
|
58
|
+
|
59
|
+
when 'rotate'
|
60
|
+
add_call_and_enter name, -arguments.first.to_f, :origin => [0, @document.y('0')]
|
61
|
+
|
62
|
+
when 'scale'
|
63
|
+
args = arguments.first.split(/\s+/)
|
64
|
+
x_scale = args[0].to_f
|
65
|
+
y_scale = (args[1] || x_scale).to_f
|
66
|
+
add_call_and_enter "transformation_matrix", x_scale, 0, 0, y_scale, 0, 0
|
67
|
+
|
68
|
+
when 'matrix'
|
69
|
+
args = arguments.first.split(/\s+/)
|
70
|
+
if args.length != 6
|
71
|
+
@document.warnings << "transform 'matrix' must have six arguments"
|
72
|
+
else
|
73
|
+
a, b, c, d, e, f = args.collect {|a| a.to_f}
|
74
|
+
add_call_and_enter "transformation_matrix", a, b, c, d, @document.distance(e), -@document.distance(f)
|
75
|
+
end
|
76
|
+
else
|
77
|
+
@document.warnings << "Unknown transformation '#{name}'; ignoring"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Opacity:
|
83
|
+
# We can't do nested opacities quite like the SVG requires, but this is close enough.
|
84
|
+
fill_opacity = stroke_opacity = clamp(@attributes['opacity'].to_f, 0, 1) if @attributes['opacity']
|
85
|
+
fill_opacity = clamp(@attributes['fill-opacity'].to_f, 0, 1) if @attributes['fill-opacity']
|
86
|
+
stroke_opacity = clamp(@attributes['stroke-opacity'].to_f, 0, 1) if @attributes['stroke-opacity']
|
87
|
+
|
88
|
+
if fill_opacity || stroke_opacity
|
89
|
+
state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1)
|
90
|
+
state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1)
|
91
|
+
|
92
|
+
add_call_and_enter 'transparent', state[:fill_opacity], state[:stroke_opacity]
|
93
|
+
end
|
94
|
+
|
95
|
+
# Fill and stroke
|
96
|
+
draw_types = []
|
97
|
+
[:fill, :stroke].each do |type|
|
98
|
+
dec = @attributes[type.to_s]
|
99
|
+
if dec == "none"
|
100
|
+
state[type] = false
|
101
|
+
elsif dec
|
102
|
+
state[type] = true
|
103
|
+
if color = color_to_hex(dec)
|
104
|
+
add_call "#{type}_color", color
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
draw_types << type.to_s if state[type]
|
109
|
+
end
|
110
|
+
|
111
|
+
# Stroke width
|
112
|
+
add_call('line_width', @document.distance(@attributes['stroke-width'])) if @attributes['stroke-width']
|
113
|
+
|
114
|
+
# Fonts
|
115
|
+
if size = @attributes['font-size']
|
116
|
+
@state[:font_size] = size.to_f * @document.scale
|
117
|
+
end
|
118
|
+
if weight = @attributes['font-weight']
|
119
|
+
font_updated = true
|
120
|
+
@state[:font_style] = weight == 'bold' ? :bold : nil
|
121
|
+
end
|
122
|
+
if (family = @attributes['font-family']) && family.strip != ""
|
123
|
+
font_updated = true
|
124
|
+
@state[:font_family] = family
|
125
|
+
end
|
126
|
+
|
127
|
+
if @state[:font_family] && font_updated
|
128
|
+
if pdf_font = Prawn::Svg::Font.map_font_family_to_pdf_font(@state[:font_family], @state[:font_style])
|
129
|
+
add_call_and_enter 'font', pdf_font
|
130
|
+
else
|
131
|
+
@document.warnings << "Font family '#{@state[:font_family]}' style '#{@state[:font_style] || 'normal'}' is not a known font."
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Call fill, stroke, or both
|
136
|
+
draw_type = draw_types.join("_and_")
|
137
|
+
if draw_type != "" && !Prawn::Svg::Parser::CONTAINER_TAGS.include?(element.name)
|
138
|
+
add_call_and_enter draw_type
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def parse_css_method_calls(string)
|
143
|
+
string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
|
144
|
+
name, argument_string = call
|
145
|
+
arguments = argument_string.split(",").collect {|s| s.strip}
|
146
|
+
[name, arguments]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords
|
151
|
+
HTML_COLORS = {
|
152
|
+
'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00",
|
153
|
+
'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00",
|
154
|
+
'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff",
|
155
|
+
'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff"
|
156
|
+
}.freeze
|
157
|
+
|
158
|
+
RGB_VALUE_REGEXP = "\s*(-?[0-9.]+%?)\s*"
|
159
|
+
RGB_REGEXP = /\Argb\(#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP}\)\z/i
|
160
|
+
|
161
|
+
def color_to_hex(color_string)
|
162
|
+
color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_|
|
163
|
+
if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i)
|
164
|
+
break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}"
|
165
|
+
elsif color.match(/\A#[0-9a-f]{6}\z/i)
|
166
|
+
break color[1..6]
|
167
|
+
elsif hex = HTML_COLORS[color.downcase]
|
168
|
+
break hex
|
169
|
+
elsif m = color.match(RGB_REGEXP)
|
170
|
+
break (1..3).collect do |n|
|
171
|
+
value = m[n].to_f
|
172
|
+
value *= 2.55 if m[n][-1..-1] == '%'
|
173
|
+
"%02x" % clamp(value.round, 0, 255)
|
174
|
+
end.join
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def clamp(value, min_value, max_value)
|
180
|
+
[[value, min_value].max, max_value].min
|
181
|
+
end
|
182
|
+
|
183
|
+
def combine_attributes_and_style_declarations
|
184
|
+
if @document && @document.css_parser
|
185
|
+
tag_style = @document.css_parser.find_by_selector(element.name)
|
186
|
+
id_style = @document.css_parser.find_by_selector("##{element.attributes["id"]}") if element.attributes["id"]
|
187
|
+
|
188
|
+
if classes = element.attributes["class"]
|
189
|
+
class_styles = classes.strip.split(/\s+/).collect do |class_name|
|
190
|
+
@document.css_parser.find_by_selector(".#{class_name}")
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
element_style = element.attributes['style']
|
195
|
+
|
196
|
+
style = [tag_style, class_styles, id_style, element_style].flatten.collect do |s|
|
197
|
+
s.nil? || s.strip == "" ? "" : "#{s}#{";" unless s.match(/;\s*\z/)}"
|
198
|
+
end.join
|
199
|
+
else
|
200
|
+
style = element.attributes['style'] || ""
|
201
|
+
end
|
202
|
+
|
203
|
+
@attributes = parse_css_declarations(style)
|
204
|
+
element.attributes.each {|n,v| @attributes[n] = v unless @attributes[n]}
|
205
|
+
end
|
206
|
+
|
207
|
+
def parse_css_declarations(declarations)
|
208
|
+
# copied from css_parser
|
209
|
+
declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')
|
210
|
+
|
211
|
+
output = {}
|
212
|
+
declarations.split(/[\;$]+/m).each do |decs|
|
213
|
+
if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
|
214
|
+
property, value, end_of_declaration = matches.captures
|
215
|
+
output[property] = value
|
216
|
+
end
|
217
|
+
end
|
218
|
+
output
|
219
|
+
end
|
220
|
+
end
|
data/lib/prawn/svg/extension.rb
CHANGED
@@ -17,7 +17,7 @@ module Prawn
|
|
17
17
|
def svg(data, options={})
|
18
18
|
svg = Prawn::Svg::Interface.new(data, self, options)
|
19
19
|
svg.draw
|
20
|
-
{:warnings => svg.
|
20
|
+
{:warnings => svg.document.warnings, :width => svg.document.width, :height => svg.document.height}
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'iconv'
|
2
|
+
|
3
|
+
class Prawn::Svg::Font
|
4
|
+
BUILT_IN_FONTS = ["Courier", "Helvetica", "Times-Roman", "Symbol", "ZapfDingbats"]
|
5
|
+
|
6
|
+
GENERIC_CSS_FONT_MAPPING = {
|
7
|
+
"serif" => "Times-Roman",
|
8
|
+
"sans-serif" => "Helvetica",
|
9
|
+
"cursive" => "Times-Roman",
|
10
|
+
"fantasy" => "Times-Roman",
|
11
|
+
"monospace" => "Courier"}
|
12
|
+
|
13
|
+
def self.map_font_family_to_pdf_font(font_family, font_style = nil)
|
14
|
+
font_family.split(",").detect do |font|
|
15
|
+
font = font.gsub(/['"]/, '').gsub(/\s{2,}/, ' ').strip.downcase
|
16
|
+
|
17
|
+
built_in_font = BUILT_IN_FONTS.detect {|f| f.downcase == font}
|
18
|
+
break built_in_font if built_in_font
|
19
|
+
|
20
|
+
generic_font = GENERIC_CSS_FONT_MAPPING[font]
|
21
|
+
break generic_font if generic_font
|
22
|
+
|
23
|
+
break font.downcase if font_installed?(font, font_style)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.font_path(font_family, font_style = nil)
|
28
|
+
font_style = :normal if font_style.nil?
|
29
|
+
if installed_styles = installed_fonts[font_family.downcase]
|
30
|
+
installed_styles[font_style]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.font_installed?(font_family, font_style = nil)
|
35
|
+
!font_path(font_family, font_style).nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.installed_fonts
|
39
|
+
return @installed_fonts if @installed_fonts
|
40
|
+
|
41
|
+
fonts = {}
|
42
|
+
Prawn::Svg::Interface.font_path.uniq.collect {|path| Dir["#{path}/*"]}.flatten.each do |filename|
|
43
|
+
information = font_information(filename) rescue nil
|
44
|
+
if information && font_name = information[1]
|
45
|
+
font_style = case information[2]
|
46
|
+
when 'Bold' then :bold
|
47
|
+
when 'Italic' then :italic
|
48
|
+
when 'Bold Italic' then :bold_italic
|
49
|
+
else :normal
|
50
|
+
end
|
51
|
+
|
52
|
+
(fonts[font_name.downcase] ||= {})[font_style] = filename
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
@installed_fonts = fonts
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.font_information(filename)
|
60
|
+
File.open(filename, "r") do |f|
|
61
|
+
x = f.read(12)
|
62
|
+
table_count = x[4] * 256 + x[5]
|
63
|
+
tables = f.read(table_count * 16)
|
64
|
+
|
65
|
+
offset, length = table_count.times do |index|
|
66
|
+
start = index * 16
|
67
|
+
if tables[start..start+3] == 'name'
|
68
|
+
break tables[start+8..start+15].unpack("NN")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
return unless length
|
73
|
+
f.seek(offset)
|
74
|
+
data = f.read(length)
|
75
|
+
|
76
|
+
format, name_count, string_offset = data[0..5].unpack("nnn")
|
77
|
+
|
78
|
+
names = {}
|
79
|
+
name_count.times do |index|
|
80
|
+
start = 6 + index * 12
|
81
|
+
platform_id, platform_specific_id, language_id, name_id, length, offset = data[start..start+11].unpack("nnnnnn")
|
82
|
+
next unless language_id == 0 # English
|
83
|
+
next unless name_id == 1 || name_id == 2
|
84
|
+
|
85
|
+
offset += string_offset
|
86
|
+
field = data[offset..offset+length-1]
|
87
|
+
names[name_id] = if platform_id == 0
|
88
|
+
begin
|
89
|
+
Iconv.iconv('UTF-8', 'UTF-16', field)
|
90
|
+
rescue
|
91
|
+
field
|
92
|
+
end
|
93
|
+
else
|
94
|
+
field
|
95
|
+
end
|
96
|
+
end
|
97
|
+
names
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/prawn/svg/interface.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#
|
2
|
-
# Prawn::Svg::Interface makes a Prawn::Svg::
|
2
|
+
# Prawn::Svg::Interface makes a Prawn::Svg::Document instance, uses that object to parse the supplied
|
3
3
|
# SVG into Prawn-compatible method calls, and then calls the Prawn methods.
|
4
4
|
#
|
5
5
|
module Prawn
|
@@ -12,12 +12,8 @@ module Prawn
|
|
12
12
|
|
13
13
|
class << self; attr_accessor :font_path; end
|
14
14
|
|
15
|
-
attr_reader :data, :prawn, :
|
15
|
+
attr_reader :data, :prawn, :document, :options
|
16
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
17
|
#
|
22
18
|
# Creates a Prawn::Svg object.
|
23
19
|
#
|
@@ -35,17 +31,18 @@ module Prawn
|
|
35
31
|
|
36
32
|
@options[:at] or raise "options[:at] must be specified"
|
37
33
|
|
38
|
-
|
39
|
-
|
34
|
+
prawn.font_families.update(Prawn::Svg::Font.installed_fonts)
|
35
|
+
|
36
|
+
@document = Document.new(data, [prawn.bounds.width, prawn.bounds.height], options)
|
40
37
|
end
|
41
38
|
|
42
39
|
#
|
43
40
|
# Draws the SVG to the Prawn::Document object.
|
44
41
|
#
|
45
42
|
def draw
|
46
|
-
prawn.bounding_box(@options[:at], :width => @
|
43
|
+
prawn.bounding_box(@options[:at], :width => @document.width, :height => @document.height) do
|
47
44
|
prawn.save_graphics_state do
|
48
|
-
proc_creator(prawn, @
|
45
|
+
proc_creator(prawn, Parser.new(@document).parse).call
|
49
46
|
end
|
50
47
|
end
|
51
48
|
end
|
@@ -71,15 +68,40 @@ module Prawn
|
|
71
68
|
end
|
72
69
|
|
73
70
|
def rewrite_call_arguments(prawn, call, arguments)
|
71
|
+
if call == 'relative_draw_text'
|
72
|
+
call.replace "draw_text"
|
73
|
+
arguments.last[:at][0] = @relative_text_position if @relative_text_position
|
74
|
+
end
|
75
|
+
|
74
76
|
case call
|
75
|
-
when '
|
76
|
-
|
77
|
-
|
77
|
+
when 'text_group'
|
78
|
+
@relative_text_position = nil
|
79
|
+
false
|
80
|
+
|
81
|
+
when 'draw_text'
|
82
|
+
text, options = arguments
|
83
|
+
|
84
|
+
width = prawn.width_of(text, options.merge(:kerning => true))
|
85
|
+
|
86
|
+
if (anchor = options.delete(:text_anchor)) && %w(middle end).include?(anchor)
|
78
87
|
width /= 2 if anchor == 'middle'
|
79
|
-
|
88
|
+
options[:at][0] -= width
|
80
89
|
end
|
81
|
-
|
82
|
-
|
90
|
+
|
91
|
+
space_width = prawn.width_of("n", options)
|
92
|
+
@relative_text_position = options[:at][0] + width + space_width
|
93
|
+
|
94
|
+
when 'transformation_matrix'
|
95
|
+
arguments[4] += prawn.bounds.absolute_left * (1 - arguments[0])
|
96
|
+
arguments[5] += prawn.bounds.absolute_top * (1 - arguments[3])
|
97
|
+
|
98
|
+
when 'save'
|
99
|
+
prawn.save_graphics_state
|
100
|
+
false
|
101
|
+
|
102
|
+
when 'restore'
|
103
|
+
prawn.restore_graphics_state
|
104
|
+
false
|
83
105
|
end
|
84
106
|
end
|
85
107
|
end
|
data/lib/prawn/svg/parser.rb
CHANGED
@@ -11,447 +11,205 @@ 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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
14
|
+
class Prawn::Svg::Parser
|
15
|
+
CONTAINER_TAGS = %w(g svg symbol defs)
|
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
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
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
|
50
43
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
56
50
|
|
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
|
73
51
|
|
52
|
+
private
|
53
|
+
REQUIRED_ATTRIBUTES = {
|
54
|
+
"line" => %w(x1 y1 x2 y2),
|
55
|
+
"polyline" => %w(points),
|
56
|
+
"polygon" => %w(points),
|
57
|
+
"circle" => %w(r),
|
58
|
+
"ellipse" => %w(rx ry),
|
59
|
+
"rect" => %w(width height),
|
60
|
+
"path" => %w(d)
|
61
|
+
}
|
74
62
|
|
75
|
-
|
76
|
-
|
77
|
-
@root = REXML::Document.new(@data).root
|
78
|
-
@actual_width, @actual_height = @bounds # set this first so % width/heights can be used
|
63
|
+
def parse_element(element)
|
64
|
+
attrs = element.attributes
|
79
65
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
66
|
+
if required_attributes = REQUIRED_ATTRIBUTES[element.name]
|
67
|
+
return unless check_attrs_present(element, required_attributes)
|
68
|
+
end
|
69
|
+
|
70
|
+
case element.name
|
71
|
+
when *CONTAINER_TAGS
|
72
|
+
element.each_child_element do |child|
|
73
|
+
element.add_call "save"
|
74
|
+
parse_element(child)
|
75
|
+
element.add_call "restore"
|
89
76
|
end
|
90
|
-
|
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
|
-
}
|
100
|
-
|
101
|
-
def parse_element(element, calls, state)
|
102
|
-
attrs = element.attributes
|
103
|
-
calls, style_attrs = apply_styles(element, calls, state)
|
104
|
-
|
105
|
-
if required_attributes = REQUIRED_ATTRIBUTES[element.name]
|
106
|
-
return unless check_attrs_present(element, required_attributes)
|
107
|
-
end
|
108
|
-
|
109
|
-
case element.name
|
110
|
-
when 'title', 'desc'
|
111
|
-
# ignore
|
112
|
-
|
113
|
-
when 'g', 'svg'
|
114
|
-
element.elements.each do |child|
|
115
|
-
parse_element(child, calls, state.dup)
|
116
|
-
end
|
117
|
-
|
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
|
123
|
-
|
124
|
-
when 'style'
|
125
|
-
load_css_styles(element)
|
126
|
-
|
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
|
139
77
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
78
|
+
do_not_append_calls = %w(symbol defs).include?(element.name)
|
79
|
+
|
80
|
+
when 'style'
|
81
|
+
load_css_styles(element)
|
82
|
+
|
83
|
+
when 'text'
|
84
|
+
@svg_text ||= Text.new
|
85
|
+
@svg_text.parse(element)
|
86
|
+
|
87
|
+
when 'line'
|
88
|
+
element.add_call 'line', x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])
|
89
|
+
|
90
|
+
when 'polyline'
|
91
|
+
points = attrs['points'].split(/\s+/)
|
92
|
+
return unless base_point = points.shift
|
93
|
+
x, y = base_point.split(",")
|
94
|
+
element.add_call 'move_to', x(x), y(y)
|
95
|
+
element.add_call_and_enter 'stroke'
|
96
|
+
points.each do |point|
|
97
|
+
x, y = point.split(",")
|
98
|
+
element.add_call "line_to", x(x), y(y)
|
99
|
+
end
|
100
|
+
|
101
|
+
when 'polygon'
|
102
|
+
points = attrs['points'].split(/\s+/).collect do |point|
|
103
|
+
x, y = point.split(",")
|
104
|
+
[x(x), y(y)]
|
105
|
+
end
|
106
|
+
element.add_call "polygon", points
|
107
|
+
|
108
|
+
when 'circle'
|
109
|
+
element.add_call "circle_at",
|
110
|
+
[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], :radius => distance(attrs['r'])
|
111
|
+
|
112
|
+
when 'ellipse'
|
113
|
+
element.add_call "ellipse_at",
|
114
|
+
[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])
|
115
|
+
|
116
|
+
when 'rect'
|
117
|
+
radius = distance(attrs['rx'] || attrs['ry'])
|
118
|
+
args = [[x(attrs['x'] || '0'), y(attrs['y'] || '0')], distance(attrs['width']), distance(attrs['height'])]
|
119
|
+
if radius
|
120
|
+
# n.b. does not support both rx and ry being specified with different values
|
121
|
+
element.add_call "rounded_rectangle", *(args + [radius])
|
122
|
+
else
|
123
|
+
element.add_call "rectangle", *args
|
124
|
+
end
|
125
|
+
|
126
|
+
when 'path'
|
127
|
+
parse_path(element)
|
150
128
|
|
151
|
-
|
129
|
+
when 'use'
|
130
|
+
parse_use(element)
|
152
131
|
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
167
|
-
|
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, []]
|
174
|
-
|
175
|
-
when 'circle'
|
176
|
-
calls << ["circle_at",
|
177
|
-
[[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}],
|
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
|
-
[]]
|
184
|
-
|
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
|
132
|
+
when 'title', 'desc', 'metadata'
|
133
|
+
# ignore
|
134
|
+
do_not_append_calls = true
|
194
135
|
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
204
|
-
|
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"
|
217
|
-
end
|
218
|
-
end
|
136
|
+
when 'font-face'
|
137
|
+
# not supported
|
138
|
+
do_not_append_calls = true
|
219
139
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
element.cdatas.collect {|d| d.to_s}.join
|
224
|
-
else
|
225
|
-
element.text
|
226
|
-
end
|
140
|
+
else
|
141
|
+
@document.warnings << "Unknown tag '#{element.name}'; ignoring"
|
142
|
+
end
|
227
143
|
|
228
|
-
|
229
|
-
|
230
|
-
end
|
231
|
-
|
232
|
-
def parse_css_declarations(declarations)
|
233
|
-
# copied from css_parser
|
234
|
-
declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')
|
235
|
-
|
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
|
242
|
-
end
|
243
|
-
output
|
244
|
-
end
|
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"]
|
250
|
-
|
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
|
256
|
-
|
257
|
-
element_style = element.attributes['style']
|
258
|
-
|
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
|
262
|
-
else
|
263
|
-
style = element.attributes['style'] || ""
|
264
|
-
end
|
144
|
+
element.append_calls_to_parent unless do_not_append_calls
|
145
|
+
end
|
265
146
|
|
266
|
-
decs = parse_css_declarations(style)
|
267
|
-
element.attributes.each {|n,v| decs[n] = v unless decs[n]}
|
268
|
-
decs
|
269
|
-
end
|
270
147
|
|
271
|
-
|
272
|
-
|
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
|
295
|
-
|
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']
|
301
|
-
|
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)
|
148
|
+
def parse_path(element)
|
149
|
+
@svg_path ||= Path.new
|
305
150
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
if color = color_to_hex(decs['fill'])
|
313
|
-
calls << ['fill_color', [color], []]
|
314
|
-
end
|
315
|
-
draw_types << 'fill'
|
316
|
-
end
|
317
|
-
|
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
|
324
|
-
|
325
|
-
calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width']
|
326
|
-
|
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
|
333
|
-
|
334
|
-
[calls, decs]
|
335
|
-
end
|
336
|
-
|
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 {|s| s.strip}
|
341
|
-
[name, arguments]
|
342
|
-
end
|
343
|
-
end
|
344
|
-
|
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"}
|
352
|
-
|
353
|
-
def installed_fonts
|
354
|
-
@installed_fonts ||= Prawn::Svg::Interface.font_path.uniq.collect {|path| Dir["#{path}/*"]}.flatten
|
355
|
-
end
|
356
|
-
|
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
|
360
|
-
|
361
|
-
built_in_font = BUILT_IN_FONTS.detect {|f| f.downcase == font}
|
362
|
-
break built_in_font if built_in_font
|
363
|
-
|
364
|
-
generic_font = GENERIC_CSS_FONT_MAPPING[font]
|
365
|
-
break generic_font if generic_font
|
366
|
-
|
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
|
372
|
-
end
|
151
|
+
begin
|
152
|
+
commands = @svg_path.parse(element.attributes['d'])
|
153
|
+
rescue Prawn::Svg::Parser::Path::InvalidError => e
|
154
|
+
commands = []
|
155
|
+
@document.warnings << e.message
|
156
|
+
end
|
373
157
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff",
|
379
|
-
'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff"
|
380
|
-
}.freeze
|
381
|
-
|
382
|
-
RGB_VALUE_REGEXP = "\s*(-?[0-9.]+%?)\s*"
|
383
|
-
RGB_REGEXP = /\Argb\(#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP}\)\z/i
|
384
|
-
|
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
|
402
|
-
|
403
|
-
def x(value)
|
404
|
-
(points(value, :x) - @x_offset) * scale
|
405
|
-
end
|
406
|
-
|
407
|
-
def y(value)
|
408
|
-
(@actual_height - (points(value, :y) - @y_offset)) * scale
|
409
|
-
end
|
410
|
-
|
411
|
-
def distance(value, axis = nil)
|
412
|
-
value && (points(value, axis) * scale)
|
413
|
-
end
|
414
|
-
|
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
|
158
|
+
commands.collect do |command, args|
|
159
|
+
point_to = [x(args[0]), y(args[1])]
|
160
|
+
if command == 'curve_to'
|
161
|
+
opts = {:bounds => [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]}
|
427
162
|
end
|
163
|
+
element.add_call command, point_to, opts
|
164
|
+
end
|
165
|
+
end
|
428
166
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
167
|
+
def parse_use(element)
|
168
|
+
if href = element.attributes['xlink:href']
|
169
|
+
if href[0..0] == '#'
|
170
|
+
id = href[1..-1]
|
171
|
+
if id_calls = element.state[:ids][id]
|
172
|
+
x = element.attributes['x']
|
173
|
+
y = element.attributes['y']
|
174
|
+
if x || y
|
175
|
+
element.add_call_and_enter "translate", distance(x || 0), -distance(y || 0)
|
176
|
+
end
|
177
|
+
|
178
|
+
element.calls.concat(id_calls)
|
436
179
|
else
|
437
|
-
@
|
180
|
+
@document.warnings << "no tag with ID '#{id}' was found, referenced by use tag"
|
438
181
|
end
|
439
|
-
|
440
|
-
@
|
441
|
-
@height ||= @actual_height * @scale
|
182
|
+
else
|
183
|
+
@document.warnings << "use tag has an href that is not a reference to an id; this is not supported"
|
442
184
|
end
|
185
|
+
else
|
186
|
+
@document.warnings << "no xlink:href specified on use tag"
|
187
|
+
end
|
188
|
+
end
|
443
189
|
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
end
|
453
|
-
missing_attrs.empty?
|
190
|
+
####################################################################################################################
|
191
|
+
|
192
|
+
def load_css_styles(element)
|
193
|
+
if @document.css_parser
|
194
|
+
data = if element.element.cdatas.any?
|
195
|
+
element.element.cdatas.collect {|d| d.to_s}.join
|
196
|
+
else
|
197
|
+
element.element.text
|
454
198
|
end
|
199
|
+
|
200
|
+
@document.css_parser.add_block!(data)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def check_attrs_present(element, attrs)
|
205
|
+
missing_attrs = attrs - element.attributes.keys
|
206
|
+
if missing_attrs.any?
|
207
|
+
@document.warnings << "Must have attributes #{missing_attrs.join(", ")} on tag #{element.name}; skipping tag"
|
455
208
|
end
|
209
|
+
missing_attrs.empty?
|
210
|
+
end
|
211
|
+
|
212
|
+
%w(x y distance).each do |method|
|
213
|
+
define_method(method) {|*a| @document.send(method, *a)}
|
456
214
|
end
|
457
215
|
end
|
@@ -0,0 +1,67 @@
|
|
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
|
+
if style = element.state[:font_style]
|
26
|
+
opts[:style] = style
|
27
|
+
end
|
28
|
+
|
29
|
+
# This is not a prawn option but we can't work out how to render it here -
|
30
|
+
# it's handled by Svg#rewrite_call_arguments
|
31
|
+
if anchor = attrs['text-anchor']
|
32
|
+
opts[:text_anchor] = anchor
|
33
|
+
end
|
34
|
+
|
35
|
+
element.element.children.each do |child|
|
36
|
+
if child.node_type == :text
|
37
|
+
text = child.to_s.strip.gsub(/\s+/, " ")
|
38
|
+
|
39
|
+
while text != ""
|
40
|
+
opts[:at] = [x_positions.first, y_positions.first]
|
41
|
+
|
42
|
+
if x_positions.length > 1 || y_positions.length > 1
|
43
|
+
element.add_call 'draw_text', text[0..0], opts.dup
|
44
|
+
text = text[1..-1]
|
45
|
+
|
46
|
+
x_positions.shift if x_positions.length > 1
|
47
|
+
y_positions.shift if y_positions.length > 1
|
48
|
+
else
|
49
|
+
element.add_call relative ? 'relative_draw_text' : 'draw_text', text, opts.dup
|
50
|
+
relative = true
|
51
|
+
break
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
elsif child.name == "tspan"
|
56
|
+
element.add_call 'save'
|
57
|
+
child_element = Prawn::Svg::Element.new(element.document, child, element.calls, element.state.dup)
|
58
|
+
internal_parse(child_element, x_positions, y_positions, relative)
|
59
|
+
child_element.append_calls_to_parent
|
60
|
+
element.add_call 'restore'
|
61
|
+
|
62
|
+
else
|
63
|
+
element.warnings << "Unknown tag '#{child.name}' inside text tag; ignoring"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
metadata
CHANGED
@@ -6,8 +6,8 @@ version: !ruby/object:Gem::Version
|
|
6
6
|
- 0
|
7
7
|
- 9
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.9.1.
|
9
|
+
- 9
|
10
|
+
version: 0.9.1.9
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Roger Nesbitt
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-06-06 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -57,9 +57,13 @@ extra_rdoc_files: []
|
|
57
57
|
files:
|
58
58
|
- README
|
59
59
|
- LICENSE
|
60
|
+
- lib/prawn/svg/document.rb
|
61
|
+
- lib/prawn/svg/element.rb
|
60
62
|
- lib/prawn/svg/extension.rb
|
63
|
+
- lib/prawn/svg/font.rb
|
61
64
|
- lib/prawn/svg/interface.rb
|
62
65
|
- lib/prawn/svg/parser/path.rb
|
66
|
+
- lib/prawn/svg/parser/text.rb
|
63
67
|
- lib/prawn/svg/parser.rb
|
64
68
|
- lib/prawn-svg.rb
|
65
69
|
has_rdoc: true
|