prawn-svg 0.9.1.8 → 0.9.1.9
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/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
|