prawn-svg 0.22.1 → 0.23.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +29 -13
- data/lib/prawn-svg.rb +7 -0
- data/lib/prawn/svg/attributes.rb +1 -1
- data/lib/prawn/svg/attributes/color.rb +5 -0
- data/lib/prawn/svg/attributes/display.rb +1 -1
- data/lib/prawn/svg/attributes/font.rb +11 -11
- data/lib/prawn/svg/attributes/opacity.rb +3 -3
- data/lib/prawn/svg/css.rb +40 -0
- data/lib/prawn/svg/document.rb +23 -8
- data/lib/prawn/svg/elements/base.rb +20 -10
- data/lib/prawn/svg/elements/container.rb +1 -1
- data/lib/prawn/svg/elements/gradient.rb +7 -4
- data/lib/prawn/svg/elements/image.rb +2 -6
- data/lib/prawn/svg/elements/root.rb +4 -0
- data/lib/prawn/svg/elements/text.rb +14 -14
- data/lib/prawn/svg/font.rb +9 -91
- data/lib/prawn/svg/font_registry.rb +73 -0
- data/lib/prawn/svg/interface.rb +9 -22
- data/lib/prawn/svg/loaders/data.rb +18 -0
- data/lib/prawn/svg/loaders/file.rb +66 -0
- data/lib/prawn/svg/loaders/web.rb +28 -0
- data/lib/prawn/svg/state.rb +39 -0
- data/lib/prawn/svg/ttf.rb +61 -0
- data/lib/prawn/svg/url_loader.rb +35 -23
- data/lib/prawn/svg/version.rb +1 -1
- data/spec/integration_spec.rb +7 -6
- data/spec/prawn/svg/attributes/font_spec.rb +8 -5
- data/spec/prawn/svg/css_spec.rb +24 -0
- data/spec/prawn/svg/document_spec.rb +35 -10
- data/spec/prawn/svg/elements/base_spec.rb +32 -10
- data/spec/prawn/svg/elements/gradient_spec.rb +1 -1
- data/spec/prawn/svg/elements/text_spec.rb +4 -4
- data/spec/prawn/svg/font_registry_spec.rb +54 -0
- data/spec/prawn/svg/font_spec.rb +0 -27
- data/spec/prawn/svg/loaders/data_spec.rb +55 -0
- data/spec/prawn/svg/loaders/file_spec.rb +84 -0
- data/spec/prawn/svg/loaders/web_spec.rb +37 -0
- data/spec/prawn/svg/ttf_spec.rb +32 -0
- data/spec/prawn/svg/url_loader_spec.rb +90 -24
- data/spec/sample_svg/image03.svg +30 -0
- data/spec/sample_svg/tspan03-cc.svg +21 -0
- data/spec/sample_ttf/OpenSans-SemiboldItalic.ttf +0 -0
- data/spec/spec_helper.rb +17 -2
- metadata +28 -2
@@ -1,4 +1,8 @@
|
|
1
1
|
class Prawn::SVG::Elements::Root < Prawn::SVG::Elements::Base
|
2
|
+
def initialize(document, source = document.root, parent_calls = [], state = ::Prawn::SVG::State.new)
|
3
|
+
super
|
4
|
+
end
|
5
|
+
|
2
6
|
def apply
|
3
7
|
add_call 'fill_color', '000000'
|
4
8
|
add_call 'transformation_matrix', @document.sizing.x_scale, 0, 0, @document.sizing.y_scale, 0, 0
|
@@ -2,12 +2,12 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
|
|
2
2
|
def parse
|
3
3
|
case attributes['xml:space']
|
4
4
|
when 'preserve'
|
5
|
-
state
|
5
|
+
state.preserve_space = true
|
6
6
|
when 'default'
|
7
|
-
state
|
7
|
+
state.preserve_space = false
|
8
8
|
end
|
9
9
|
|
10
|
-
@relative = state
|
10
|
+
@relative = state.text_relative || false
|
11
11
|
|
12
12
|
if attributes['x'] || attributes['y']
|
13
13
|
@relative = false
|
@@ -15,12 +15,12 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
|
|
15
15
|
@y_positions = attributes['y'].split(COMMA_WSP_REGEXP).collect {|n| document.y(n)} if attributes['y']
|
16
16
|
end
|
17
17
|
|
18
|
-
@x_positions ||= state
|
19
|
-
@y_positions ||= state
|
18
|
+
@x_positions ||= state.text_x_positions || [document.x(0)]
|
19
|
+
@y_positions ||= state.text_y_positions || [document.y(0)]
|
20
20
|
end
|
21
21
|
|
22
22
|
def apply
|
23
|
-
raise SkipElementQuietly if state
|
23
|
+
raise SkipElementQuietly if state.display == "none"
|
24
24
|
|
25
25
|
add_call_and_enter "text_group" if name == 'text'
|
26
26
|
|
@@ -29,14 +29,14 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
|
|
29
29
|
end
|
30
30
|
|
31
31
|
opts = {}
|
32
|
-
if size = state
|
32
|
+
if size = state.font_size
|
33
33
|
opts[:size] = size
|
34
34
|
end
|
35
|
-
opts[:style] = state
|
35
|
+
opts[:style] = state.font_subfamily
|
36
36
|
|
37
37
|
# This is not a prawn option but we can't work out how to render it here -
|
38
38
|
# it's handled by SVG#rewrite_call_arguments
|
39
|
-
if (anchor = attributes['text-anchor'] || state
|
39
|
+
if (anchor = attributes['text-anchor'] || state.text_anchor) &&
|
40
40
|
['start', 'middle', 'end'].include?(anchor)
|
41
41
|
opts[:text_anchor] = anchor
|
42
42
|
end
|
@@ -47,7 +47,7 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
|
|
47
47
|
|
48
48
|
source.children.each do |child|
|
49
49
|
if child.node_type == :text
|
50
|
-
text = child.value.strip.gsub(state
|
50
|
+
text = child.value.strip.gsub(state.preserve_space ? /[\n\t]/ : /\s+/, " ")
|
51
51
|
|
52
52
|
while text != ""
|
53
53
|
opts[:at] = [@x_positions.first, @y_positions.first]
|
@@ -70,10 +70,10 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
|
|
70
70
|
add_call 'save'
|
71
71
|
|
72
72
|
new_state = state.dup
|
73
|
-
new_state
|
74
|
-
new_state
|
75
|
-
new_state
|
76
|
-
new_state
|
73
|
+
new_state.text_x_positions = @x_positions
|
74
|
+
new_state.text_y_positions = @y_positions
|
75
|
+
new_state.text_relative = @relative
|
76
|
+
new_state.text_anchor = opts[:text_anchor]
|
77
77
|
|
78
78
|
Prawn::SVG::Elements::Text.new(document, child, calls, new_state).process
|
79
79
|
|
data/lib/prawn/svg/font.rb
CHANGED
@@ -4,22 +4,11 @@ class Prawn::SVG::Font
|
|
4
4
|
"sans-serif" => "Helvetica",
|
5
5
|
"cursive" => "Times-Roman",
|
6
6
|
"fantasy" => "Times-Roman",
|
7
|
-
"monospace" => "Courier"
|
7
|
+
"monospace" => "Courier"
|
8
|
+
}
|
8
9
|
|
9
10
|
attr_reader :name, :weight, :style
|
10
11
|
|
11
|
-
def self.load(family, weight = nil, style = nil)
|
12
|
-
family.split(",").detect do |name|
|
13
|
-
name = name.gsub(/['"]/, '').gsub(/\s{2,}/, ' ').strip.downcase
|
14
|
-
|
15
|
-
# If it's a standard CSS font name, map it to one of the standard PDF fonts.
|
16
|
-
name = GENERIC_CSS_FONT_MAPPING[name] || name
|
17
|
-
|
18
|
-
font = new(name, weight, style)
|
19
|
-
break font if font.installed?
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
12
|
def self.weight_for_css_font_weight(weight)
|
24
13
|
case weight
|
25
14
|
when '100', '200', '300' then :light
|
@@ -31,47 +20,23 @@ class Prawn::SVG::Font
|
|
31
20
|
end
|
32
21
|
end
|
33
22
|
|
34
|
-
|
35
|
-
|
36
|
-
def self.load_external_fonts(fonts)
|
37
|
-
Prawn::SVG::Interface.font_path.uniq.collect {|path| Dir["#{path}/**/*"]}.flatten.each do |filename|
|
38
|
-
information = font_information(filename) rescue nil
|
39
|
-
if information && font_name = (information[16] || information[1])
|
40
|
-
subfamily = (information[17] || information[2] || "normal").gsub(/\s+/, "_").downcase.to_sym
|
41
|
-
subfamily = :normal if subfamily == :regular
|
42
|
-
(fonts[font_name] ||= {})[subfamily] ||= filename
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
@font_case_mapping = {}
|
47
|
-
fonts.each {|key, _| @font_case_mapping[key.downcase] = key}
|
48
|
-
|
49
|
-
@installed_fonts = fonts
|
50
|
-
end
|
51
|
-
|
52
|
-
def self.installed_fonts
|
53
|
-
@installed_fonts
|
54
|
-
end
|
55
|
-
|
56
|
-
def self.correctly_cased_font_name(name)
|
57
|
-
@font_case_mapping[name.downcase]
|
58
|
-
end
|
23
|
+
def initialize(name, weight, style, font_registry: font_registry)
|
24
|
+
name = GENERIC_CSS_FONT_MAPPING.fetch(name, name) # If it's a standard CSS font name, map it to one of the standard PDF fonts.
|
59
25
|
|
60
|
-
|
61
|
-
|
62
|
-
@name = self.class.correctly_cased_font_name(name) || name
|
26
|
+
@font_registry = font_registry
|
27
|
+
@name = font_registry.correctly_cased_font_name(name) || name
|
63
28
|
@weight = weight
|
64
29
|
@style = style
|
65
30
|
end
|
66
31
|
|
67
32
|
def installed?
|
68
|
-
subfamilies =
|
33
|
+
subfamilies = @font_registry.installed_fonts[name]
|
69
34
|
!subfamilies.nil? && subfamilies.key?(subfamily)
|
70
35
|
end
|
71
36
|
|
72
37
|
# Construct a subfamily name, ensuring that the subfamily is a valid one for the font.
|
73
38
|
def subfamily
|
74
|
-
if subfamilies =
|
39
|
+
if subfamilies = @font_registry.installed_fonts[name]
|
75
40
|
if subfamilies.key?(subfamily_name)
|
76
41
|
subfamily_name
|
77
42
|
elsif subfamilies.key?(:normal)
|
@@ -82,8 +47,8 @@ class Prawn::SVG::Font
|
|
82
47
|
end
|
83
48
|
end
|
84
49
|
|
85
|
-
|
86
50
|
private
|
51
|
+
|
87
52
|
# Construct a subfamily name from the weight and style information.
|
88
53
|
# Note that this name might not actually exist in the font.
|
89
54
|
def subfamily_name
|
@@ -97,51 +62,4 @@ class Prawn::SVG::Font
|
|
97
62
|
|
98
63
|
sfn.strip.gsub(/\s+/, "_").downcase.to_sym
|
99
64
|
end
|
100
|
-
|
101
|
-
def self.font_information(filename)
|
102
|
-
File.open(filename, "r") do |f|
|
103
|
-
x = f.read(12)
|
104
|
-
table_count = x[4].ord * 256 + x[5].ord
|
105
|
-
tables = f.read(table_count * 16)
|
106
|
-
|
107
|
-
offset, length = table_count.times do |index|
|
108
|
-
start = index * 16
|
109
|
-
if tables[start..start+3] == 'name'
|
110
|
-
break tables[start+8..start+15].unpack("NN")
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
return unless length
|
115
|
-
f.seek(offset)
|
116
|
-
data = f.read(length)
|
117
|
-
|
118
|
-
format, name_count, string_offset = data[0..5].unpack("nnn")
|
119
|
-
|
120
|
-
names = {}
|
121
|
-
name_count.times do |index|
|
122
|
-
start = 6 + index * 12
|
123
|
-
platform_id, platform_specific_id, language_id, name_id, length, offset = data[start..start+11].unpack("nnnnnn")
|
124
|
-
next unless language_id == 0 # English
|
125
|
-
next unless [1, 2, 16, 17].include?(name_id)
|
126
|
-
|
127
|
-
offset += string_offset
|
128
|
-
field = data[offset..offset+length-1]
|
129
|
-
names[name_id] = if platform_id == 0
|
130
|
-
begin
|
131
|
-
if field.respond_to?(:encode)
|
132
|
-
field.encode(Encoding::UTF16)
|
133
|
-
else
|
134
|
-
require "iconv"
|
135
|
-
Iconv.iconv('UTF-8', 'UTF-16', field)
|
136
|
-
end
|
137
|
-
rescue
|
138
|
-
field
|
139
|
-
end
|
140
|
-
else
|
141
|
-
field
|
142
|
-
end
|
143
|
-
end
|
144
|
-
names
|
145
|
-
end
|
146
|
-
end
|
147
65
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
class Prawn::SVG::FontRegistry
|
2
|
+
DEFAULT_FONT_PATHS = [
|
3
|
+
"/Library/Fonts",
|
4
|
+
"/System/Library/Fonts",
|
5
|
+
"#{ENV["HOME"]}/Library/Fonts",
|
6
|
+
"/usr/share/fonts/truetype"
|
7
|
+
]
|
8
|
+
|
9
|
+
@font_path = DEFAULT_FONT_PATHS.select { |path| Dir.exist?(path) }
|
10
|
+
|
11
|
+
def initialize(font_families)
|
12
|
+
@font_families = font_families
|
13
|
+
end
|
14
|
+
|
15
|
+
def installed_fonts
|
16
|
+
merge_external_fonts
|
17
|
+
@font_families
|
18
|
+
end
|
19
|
+
|
20
|
+
def correctly_cased_font_name(name)
|
21
|
+
merge_external_fonts
|
22
|
+
@font_case_mapping[name.downcase]
|
23
|
+
end
|
24
|
+
|
25
|
+
def load(family, weight = nil, style = nil)
|
26
|
+
Prawn::SVG::CSS.parse_font_family_string(family).detect do |name|
|
27
|
+
name = name.gsub(/\s{2,}/, ' ').downcase
|
28
|
+
|
29
|
+
font = Prawn::SVG::Font.new(name, weight, style, font_registry: self)
|
30
|
+
break font if font.installed?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def merge_external_fonts
|
37
|
+
if @font_case_mapping.nil?
|
38
|
+
self.class.load_external_fonts unless self.class.external_font_families
|
39
|
+
@font_families.merge!(self.class.external_font_families)
|
40
|
+
|
41
|
+
@font_case_mapping = @font_families.keys.each.with_object({}) do |key, result|
|
42
|
+
result[key.downcase] = key
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
attr_reader :external_font_families, :font_path
|
49
|
+
|
50
|
+
def load_external_fonts
|
51
|
+
@external_font_families = {}
|
52
|
+
|
53
|
+
external_font_paths.each do |filename|
|
54
|
+
ttf = Prawn::SVG::TTF.new(filename)
|
55
|
+
if ttf.family
|
56
|
+
subfamily = (ttf.subfamily || "normal").gsub(/\s+/, "_").downcase.to_sym
|
57
|
+
subfamily = :normal if subfamily == :regular
|
58
|
+
(external_font_families[ttf.family] ||= {})[subfamily] ||= filename
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def external_font_paths
|
66
|
+
font_path
|
67
|
+
.uniq
|
68
|
+
.flat_map { |path| Dir["#{path}/**/*"] }
|
69
|
+
.uniq
|
70
|
+
.select { |path| File.file?(path) }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/prawn/svg/interface.rb
CHANGED
@@ -5,14 +5,7 @@
|
|
5
5
|
module Prawn
|
6
6
|
module SVG
|
7
7
|
class Interface
|
8
|
-
VALID_OPTIONS = [:at, :position, :vposition, :width, :height, :cache_images, :fallback_font_name]
|
9
|
-
|
10
|
-
DEFAULT_FONT_PATHS = ["/Library/Fonts", "/System/Library/Fonts", "#{ENV["HOME"]}/Library/Fonts", "/usr/share/fonts/truetype"]
|
11
|
-
|
12
|
-
@font_path = []
|
13
|
-
DEFAULT_FONT_PATHS.each {|path| @font_path << path if File.exists?(path)}
|
14
|
-
|
15
|
-
class << self; attr_accessor :font_path; end
|
8
|
+
VALID_OPTIONS = [:at, :position, :vposition, :width, :height, :cache_images, :enable_web_requests, :enable_file_requests_with_root, :fallback_font_name]
|
16
9
|
|
17
10
|
attr_reader :data, :prawn, :document, :options
|
18
11
|
|
@@ -21,17 +14,7 @@ module Prawn
|
|
21
14
|
#
|
22
15
|
# +data+ is the SVG data to convert. +prawn+ is your Prawn::Document object.
|
23
16
|
#
|
24
|
-
#
|
25
|
-
# <tt>:at</tt>:: an array [x,y] specifying the location of the top-left corner of the SVG.
|
26
|
-
# <tt>:position</tt>:: one of (nil, :left, :center, :right) or an x-offset
|
27
|
-
# <tt>:vposition</tt>:: one of (nil, :top, :center, :bottom) or a y-offset
|
28
|
-
# <tt>:width</tt>:: the width that the SVG is to be rendered
|
29
|
-
# <tt>:height</tt>:: the height that the SVG is to be rendered
|
30
|
-
#
|
31
|
-
# If <tt>:at</tt> is provided, the SVG will be placed in the current page but
|
32
|
-
# the text position will not be changed.
|
33
|
-
#
|
34
|
-
# If both <tt>:width</tt> and <tt>:height</tt> are specified, only the width will be used.
|
17
|
+
# See README.md for the options that can be passed to this method.
|
35
18
|
#
|
36
19
|
def initialize(data, prawn, options, &block)
|
37
20
|
Prawn.verify_options VALID_OPTIONS, options
|
@@ -40,9 +23,9 @@ module Prawn
|
|
40
23
|
@prawn = prawn
|
41
24
|
@options = options
|
42
25
|
|
43
|
-
Prawn::SVG::
|
26
|
+
font_registry = Prawn::SVG::FontRegistry.new(prawn.font_families)
|
44
27
|
|
45
|
-
@document = Document.new(data, [prawn.bounds.width, prawn.bounds.height], options, &block)
|
28
|
+
@document = Document.new(data, [prawn.bounds.width, prawn.bounds.height], options, font_registry: font_registry, &block)
|
46
29
|
end
|
47
30
|
|
48
31
|
#
|
@@ -61,7 +44,7 @@ module Prawn
|
|
61
44
|
clip_rectangle 0, 0, @document.sizing.output_width, @document.sizing.output_height
|
62
45
|
|
63
46
|
calls = []
|
64
|
-
root_element = Prawn::SVG::Elements::Root.new(@document, @document.root, calls
|
47
|
+
root_element = Prawn::SVG::Elements::Root.new(@document, @document.root, calls)
|
65
48
|
root_element.process
|
66
49
|
|
67
50
|
proc_creator(prawn, calls).call
|
@@ -73,6 +56,10 @@ module Prawn
|
|
73
56
|
@options[:at] || [x_based_on_requested_alignment, y_based_on_requested_alignment]
|
74
57
|
end
|
75
58
|
|
59
|
+
def self.font_path # backwards support for when the font_path used to be stored on this class
|
60
|
+
Prawn::SVG::FontRegistry.font_path
|
61
|
+
end
|
62
|
+
|
76
63
|
private
|
77
64
|
|
78
65
|
def x_based_on_requested_alignment
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Prawn::SVG::Loaders
|
4
|
+
class Data
|
5
|
+
REGEXP = %r[\A(?i:data):image/(png|jpeg);base64(;[a-z0-9]+)*,]
|
6
|
+
|
7
|
+
def from_url(url)
|
8
|
+
return if url[0..4].downcase != "data:"
|
9
|
+
|
10
|
+
matches = url.match(REGEXP)
|
11
|
+
if matches.nil?
|
12
|
+
raise Prawn::SVG::UrlLoader::Error, "prawn-svg only supports base64-encoded image/png and image/jpeg data URLs"
|
13
|
+
end
|
14
|
+
|
15
|
+
Base64.decode64(matches.post_match)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Prawn::SVG::Loaders
|
2
|
+
class File
|
3
|
+
attr_reader :root_path
|
4
|
+
|
5
|
+
def initialize(root_path)
|
6
|
+
@root_path = ::File.expand_path(root_path)
|
7
|
+
|
8
|
+
raise ArgumentError, "#{root_path} is not a directory" unless Dir.exist?(@root_path)
|
9
|
+
end
|
10
|
+
|
11
|
+
def from_url(url)
|
12
|
+
uri = build_uri(url)
|
13
|
+
|
14
|
+
if uri && uri.scheme.nil? && uri.path
|
15
|
+
path = build_absolute_path(uri.path)
|
16
|
+
load_file(path)
|
17
|
+
|
18
|
+
elsif uri && uri.scheme == 'file'
|
19
|
+
assert_valid_file_uri!(uri)
|
20
|
+
load_file(uri.path)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def load_file(path)
|
27
|
+
path = ::File.expand_path(path)
|
28
|
+
assert_valid_path!(path)
|
29
|
+
assert_file_exists!(path)
|
30
|
+
IO.read(path)
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_uri(url)
|
34
|
+
begin
|
35
|
+
URI(url)
|
36
|
+
rescue URI::InvalidURIError
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def assert_valid_path!(path)
|
41
|
+
if !path.start_with?("#{root_path}/")
|
42
|
+
raise Prawn::SVG::UrlLoader::Error, "file path is not inside the root path of #{root_path}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_absolute_path(path)
|
47
|
+
if path[0] == "/"
|
48
|
+
path
|
49
|
+
else
|
50
|
+
"#{root_path}/#{path}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def assert_valid_file_uri!(uri)
|
55
|
+
if uri.host
|
56
|
+
raise Prawn::SVG::UrlLoader::Error, "prawn-svg does not suport file: URLs with a host. Your URL probably doesn't start with three slashes, and it should."
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def assert_file_exists!(path)
|
61
|
+
if !::File.exist?(path)
|
62
|
+
raise Prawn::SVG::UrlLoader::Error, "File #{path} does not exist"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|