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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -13
  3. data/lib/prawn-svg.rb +7 -0
  4. data/lib/prawn/svg/attributes.rb +1 -1
  5. data/lib/prawn/svg/attributes/color.rb +5 -0
  6. data/lib/prawn/svg/attributes/display.rb +1 -1
  7. data/lib/prawn/svg/attributes/font.rb +11 -11
  8. data/lib/prawn/svg/attributes/opacity.rb +3 -3
  9. data/lib/prawn/svg/css.rb +40 -0
  10. data/lib/prawn/svg/document.rb +23 -8
  11. data/lib/prawn/svg/elements/base.rb +20 -10
  12. data/lib/prawn/svg/elements/container.rb +1 -1
  13. data/lib/prawn/svg/elements/gradient.rb +7 -4
  14. data/lib/prawn/svg/elements/image.rb +2 -6
  15. data/lib/prawn/svg/elements/root.rb +4 -0
  16. data/lib/prawn/svg/elements/text.rb +14 -14
  17. data/lib/prawn/svg/font.rb +9 -91
  18. data/lib/prawn/svg/font_registry.rb +73 -0
  19. data/lib/prawn/svg/interface.rb +9 -22
  20. data/lib/prawn/svg/loaders/data.rb +18 -0
  21. data/lib/prawn/svg/loaders/file.rb +66 -0
  22. data/lib/prawn/svg/loaders/web.rb +28 -0
  23. data/lib/prawn/svg/state.rb +39 -0
  24. data/lib/prawn/svg/ttf.rb +61 -0
  25. data/lib/prawn/svg/url_loader.rb +35 -23
  26. data/lib/prawn/svg/version.rb +1 -1
  27. data/spec/integration_spec.rb +7 -6
  28. data/spec/prawn/svg/attributes/font_spec.rb +8 -5
  29. data/spec/prawn/svg/css_spec.rb +24 -0
  30. data/spec/prawn/svg/document_spec.rb +35 -10
  31. data/spec/prawn/svg/elements/base_spec.rb +32 -10
  32. data/spec/prawn/svg/elements/gradient_spec.rb +1 -1
  33. data/spec/prawn/svg/elements/text_spec.rb +4 -4
  34. data/spec/prawn/svg/font_registry_spec.rb +54 -0
  35. data/spec/prawn/svg/font_spec.rb +0 -27
  36. data/spec/prawn/svg/loaders/data_spec.rb +55 -0
  37. data/spec/prawn/svg/loaders/file_spec.rb +84 -0
  38. data/spec/prawn/svg/loaders/web_spec.rb +37 -0
  39. data/spec/prawn/svg/ttf_spec.rb +32 -0
  40. data/spec/prawn/svg/url_loader_spec.rb +90 -24
  41. data/spec/sample_svg/image03.svg +30 -0
  42. data/spec/sample_svg/tspan03-cc.svg +21 -0
  43. data/spec/sample_ttf/OpenSans-SemiboldItalic.ttf +0 -0
  44. data/spec/spec_helper.rb +17 -2
  45. 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[:preserve_space] = true
5
+ state.preserve_space = true
6
6
  when 'default'
7
- state[:preserve_space] = false
7
+ state.preserve_space = false
8
8
  end
9
9
 
10
- @relative = state[:text_relative] || false
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[:text_x_positions] || [document.x(0)]
19
- @y_positions ||= state[:text_y_positions] || [document.y(0)]
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[:display] == "none"
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[:font_size]
32
+ if size = state.font_size
33
33
  opts[:size] = size
34
34
  end
35
- opts[:style] = state[:font_subfamily]
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[:text_anchor]) &&
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[:preserve_space] ? /[\n\t]/ : /\s+/, " ")
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[: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]
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
 
@@ -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
- # This method is passed prawn's font_families hash. It'll be pre-populated with the fonts that prawn natively
35
- # supports. We'll add fonts we find in the font path to this hash.
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
- def initialize(name, weight, style)
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 = self.class.installed_fonts[name]
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 = self.class.installed_fonts[name]
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
@@ -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
- # Options:
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::Font.load_external_fonts(prawn.font_families)
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, fill: true)
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