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.
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