prawn-svg 0.22.1 → 0.23.0
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.
- 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
|