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
@@ -0,0 +1,28 @@
1
+ require 'net/http'
2
+
3
+ module Prawn::SVG::Loaders
4
+ class Web
5
+ def from_url(url)
6
+ uri = build_uri(url)
7
+
8
+ if uri && %w(http https).include?(uri.scheme)
9
+ perform_request(uri)
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def build_uri(url)
16
+ begin
17
+ URI(url)
18
+ rescue URI::InvalidURIError
19
+ end
20
+ end
21
+
22
+ def perform_request(uri)
23
+ Net::HTTP.get(uri)
24
+ rescue => e
25
+ raise Prawn::SVG::UrlLoader::Error, e.message
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ class Prawn::SVG::State
2
+ attr_accessor :disable_drawing,
3
+ :color, :display,
4
+ :font_size, :font_weight, :font_style, :font_family, :font_subfamily,
5
+ :text_anchor, :text_relative, :text_x_positions, :text_y_positions, :preserve_space,
6
+ :fill_opacity, :stroke_opacity,
7
+ :fill, :stroke
8
+
9
+ def initialize
10
+ @fill = true
11
+ @stroke = false
12
+ @fill_opacity = 1
13
+ @stroke_opacity = 1
14
+ end
15
+
16
+ def enable_draw_type(type)
17
+ case type
18
+ when 'fill' then @fill = true
19
+ when 'stroke' then @stroke = true
20
+ else raise
21
+ end
22
+ end
23
+
24
+ def disable_draw_type(type)
25
+ case type
26
+ when 'fill' then @fill = false
27
+ when 'stroke' then @stroke = false
28
+ else raise
29
+ end
30
+ end
31
+
32
+ def draw_type(type)
33
+ case type
34
+ when 'fill' then @fill
35
+ when 'stroke' then @stroke
36
+ else raise
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,61 @@
1
+ class Prawn::SVG::TTF
2
+ SFNT_VERSION_STRINGS = ["\x00\x01\x00\x00", "true", "typ1"]
3
+ LANGUAGE_IDS = [0, 0x409] # English, US English
4
+ UTF_16BE_PLATFORM_IDS = [0, 3] # Unicode, Microsoft
5
+
6
+ attr_reader :family, :subfamily
7
+
8
+ def initialize(filename)
9
+ load_data_from_file(filename)
10
+ end
11
+
12
+ private
13
+
14
+ def load_data_from_file(filename)
15
+ File.open(filename, "rb") do |f|
16
+ offset_table = f.read(12)
17
+ return unless offset_table && offset_table.length == 12 && SFNT_VERSION_STRINGS.include?(offset_table[0..3])
18
+
19
+ table_count = offset_table[4].ord * 256 + offset_table[5].ord
20
+ tables = f.read(table_count * 16)
21
+ return unless tables && tables.length == table_count * 16
22
+
23
+ offset, length = table_count.times do |index|
24
+ start = index * 16
25
+ if tables[start..start+3] == 'name'
26
+ break tables[start+8..start+15].unpack("NNN")
27
+ end
28
+ end
29
+
30
+ return unless length
31
+ f.seek(offset)
32
+ data = f.read(length)
33
+ return unless data && data.length == length
34
+
35
+ format, name_count, string_offset = data[0..5].unpack("nnn")
36
+
37
+ names = {}
38
+ name_count.times do |index|
39
+ start = 6 + index * 12
40
+ platform_id, platform_specific_id, language_id, name_id, length, offset = data[start..start+11].unpack("nnnnnn")
41
+ next unless offset
42
+ next unless LANGUAGE_IDS.include?(language_id)
43
+ next unless [1, 2, 16, 17].include?(name_id)
44
+
45
+ offset += string_offset
46
+ field = data[offset..offset+length-1]
47
+ next unless field && field.length == length
48
+
49
+ names[name_id] = if UTF_16BE_PLATFORM_IDS.include?(platform_id)
50
+ field.force_encoding(Encoding::UTF_16BE).encode(Encoding::UTF_8) rescue field
51
+ else
52
+ field
53
+ end
54
+ end
55
+
56
+ @family = names[16] || names[1]
57
+ @subfamily = names[17] || names[2]
58
+ end
59
+ rescue Errno::ENOENT # in case the file disappears between the scan and the load, we don't want to crash
60
+ end
61
+ end
@@ -1,34 +1,46 @@
1
- require 'open-uri'
2
- require 'base64'
3
-
4
1
  class Prawn::SVG::UrlLoader
5
- attr_accessor :enable_cache, :enable_web
6
- attr_reader :url_cache
2
+ Error = Class.new(StandardError)
7
3
 
8
- DATAURL_REGEXP = /(data:image\/(png|jpg);base64(;[a-z0-9]+)*,)/
9
- URL_REGEXP = /^https?:\/\/|#{DATAURL_REGEXP}/
4
+ attr_reader :enable_cache, :loaders
10
5
 
11
- def initialize(opts = {})
6
+ def initialize(enable_cache: false, enable_web: true, enable_file_with_root: nil)
12
7
  @url_cache = {}
13
- @enable_cache = opts.fetch(:enable_cache, false)
14
- @enable_web = opts.fetch(:enable_web, true)
15
- end
8
+ @enable_cache = enable_cache
16
9
 
17
- def valid?(url)
18
- !!url.match(URL_REGEXP)
10
+ @loaders = []
11
+ loaders << Prawn::SVG::Loaders::Data.new
12
+ loaders << Prawn::SVG::Loaders::Web.new if enable_web
13
+ loaders << Prawn::SVG::Loaders::File.new(enable_file_with_root) if enable_file_with_root
19
14
  end
20
15
 
21
16
  def load(url)
22
- @url_cache[url] || begin
23
- if m = url.match(DATAURL_REGEXP)
24
- data = Base64.decode64(url[m[0].length .. -1])
25
- elsif enable_web
26
- data = open(url).read
27
- else
28
- raise "No handler available to retrieve URL #{url}"
29
- end
30
- @url_cache[url] = data if enable_cache
31
- data
17
+ retrieve_from_cache(url) || perform_and_cache(url)
18
+ end
19
+
20
+ def add_to_cache(url, data)
21
+ @url_cache[url] = data
22
+ end
23
+
24
+ def retrieve_from_cache(url)
25
+ @url_cache[url]
26
+ end
27
+
28
+ private
29
+
30
+ def perform_and_cache(url)
31
+ data = perform(url)
32
+ add_to_cache(url, data) if enable_cache
33
+ data
34
+ end
35
+
36
+ def perform(url)
37
+ try_each_loader(url) or raise Error, "No handler available for this URL scheme"
38
+ end
39
+
40
+ def try_each_loader(url)
41
+ loaders.detect do |loader|
42
+ data = loader.from_url(url)
43
+ break data if data
32
44
  end
33
45
  end
34
46
  end
@@ -1,5 +1,5 @@
1
1
  module Prawn
2
2
  module SVG
3
- VERSION = '0.22.1'
3
+ VERSION = '0.23.0'
4
4
  end
5
5
  end
@@ -5,7 +5,7 @@ describe "Integration test" do
5
5
 
6
6
  describe "a basic SVG file" do
7
7
  let(:document) { Prawn::SVG::Document.new(svg, [800, 600], {}) }
8
- let(:element) { Prawn::SVG::Elements::Root.new(document, document.root, [], {}) }
8
+ let(:element) { Prawn::SVG::Elements::Root.new(document) }
9
9
 
10
10
  let(:svg) do
11
11
  <<-SVG
@@ -100,15 +100,16 @@ describe "Integration test" do
100
100
 
101
101
  files.each do |file|
102
102
  it "renders the #{File.basename file} sample file without warnings or crashing" do
103
+ expect(Net::HTTP).to_not receive(:get)
104
+
103
105
  warnings = nil
104
106
  Prawn::Document.generate("#{root}/spec/sample_output/#{File.basename file}.pdf") do |prawn|
105
- r = prawn.svg IO.read(file), :at => [0, prawn.bounds.top], :width => prawn.bounds.width do |doc|
106
- doc.url_loader.enable_web = false
107
- doc.url_loader.url_cache["https://raw.githubusercontent.com/mogest/prawn-svg/master/spec/sample_images/mushroom-wide.jpg"] = IO.read("#{root}/spec/sample_images/mushroom-wide.jpg")
108
- doc.url_loader.url_cache["https://raw.githubusercontent.com/mogest/prawn-svg/master/spec/sample_images/mushroom-long.jpg"] = IO.read("#{root}/spec/sample_images/mushroom-long.jpg")
107
+ r = prawn.svg IO.read(file), :at => [0, prawn.bounds.top], :width => prawn.bounds.width, :enable_file_requests_with_root => File.dirname(__FILE__) do |doc|
108
+ doc.url_loader.add_to_cache("https://raw.githubusercontent.com/mogest/prawn-svg/master/spec/sample_images/mushroom-wide.jpg", IO.read("#{root}/spec/sample_images/mushroom-wide.jpg"))
109
+ doc.url_loader.add_to_cache("https://raw.githubusercontent.com/mogest/prawn-svg/master/spec/sample_images/mushroom-long.jpg", IO.read("#{root}/spec/sample_images/mushroom-long.jpg"))
109
110
  end
110
111
 
111
- warnings = r[:warnings].reject {|w| w =~ /Verdana/ && w =~ /is not a known font/ || w =~ /render gradients$/}
112
+ warnings = r[:warnings].reject {|w| w =~ /Verdana/ && w =~ /is not a known font/ || w =~ /(render gradients$|waiting on the Prawn project)/}
112
113
  end
113
114
  warnings.should == []
114
115
  end
@@ -4,16 +4,19 @@ describe Prawn::SVG::Attributes::Font do
4
4
  class FontTestElement
5
5
  include Prawn::SVG::Attributes::Font
6
6
 
7
- attr_accessor :attributes, :warnings
7
+ attr_accessor :attributes, :warnings, :state, :document
8
8
 
9
- def initialize
10
- @state = {}
9
+ def initialize(document)
10
+ @state = Prawn::SVG::State.new
11
+ @document = document
11
12
  @warnings = []
12
13
  end
13
14
  end
14
15
 
15
- let(:element) { FontTestElement.new }
16
- let(:document) { double(fallback_font_name: "Times-Roman") }
16
+ let(:pdf) { Prawn::Document.new }
17
+ let(:font_registry) { Prawn::SVG::FontRegistry.new(pdf.font_families) }
18
+ let(:document) { double(fallback_font_name: "Times-Roman", font_registry: font_registry) }
19
+ let(:element) { FontTestElement.new(document) }
17
20
 
18
21
  before do
19
22
  allow(element).to receive(:document).and_return(document)
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Prawn::SVG::CSS do
4
+ describe "#parse_font_family_string" do
5
+ it "correctly handles quotes and escaping" do
6
+ tests = {
7
+ "" => [],
8
+ "font" => ["font"],
9
+ "font name, other font" => ["font name", "other font"],
10
+ "'font name', other font" => ["font name", "other font"],
11
+ "'font, name', other font" => ["font, name", "other font"],
12
+ '"font name", other font' => ["font name", "other font"],
13
+ '"font, name", other font' => ["font, name", "other font"],
14
+ 'weird \\" name' => ['weird " name'],
15
+ 'weird\\, name' => ["weird, name"],
16
+ ' stupid , spacing ' => ["stupid", "spacing"],
17
+ }
18
+
19
+ tests.each do |string, expected|
20
+ expect(Prawn::SVG::CSS.parse_font_family_string(string)).to eq expected
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,21 +1,46 @@
1
1
  require File.dirname(__FILE__) + '/../../spec_helper'
2
2
 
3
- describe Prawn::SVG::Document do
4
- before do
5
- sizing = instance_double(Prawn::SVG::Calculators::DocumentSizing, viewport_width: 600, viewport_height: 400, viewport_diagonal: 500, :requested_width= => nil, :requested_height= => nil)
6
- expect(sizing).to receive(:calculate)
7
- expect(Prawn::SVG::Calculators::DocumentSizing).to receive(:new).and_return(sizing)
3
+ describe Prawn::SVG::Document do
4
+ let(:bounds) { [100, 100] }
5
+ let(:options) { {} }
6
+
7
+ describe "#initialize" do
8
+ context "when unparsable XML is provided" do
9
+ let(:svg) { "this isn't SVG data" }
10
+
11
+ it "raises an exception" do
12
+ expect {
13
+ Prawn::SVG::Document.new(svg, bounds, options)
14
+ }.to raise_error Prawn::SVG::Document::InvalidSVGData, "The data supplied is not a valid SVG document."
15
+ end
16
+ end
17
+
18
+ context "when the user passes in a filename instead of SVG data" do
19
+ let(:svg) { "some_file.svg" }
20
+
21
+ it "raises an exception letting them know what they've done" do
22
+ expect {
23
+ Prawn::SVG::Document.new(svg, bounds, options)
24
+ }.to raise_error Prawn::SVG::Document::InvalidSVGData, "The data supplied is not a valid SVG document. It looks like you've supplied a filename instead; use IO.read(filename) to get the data before you pass it to prawn-svg."
25
+ end
26
+ end
8
27
  end
9
28
 
10
- let(:document) { Prawn::SVG::Document.new("<svg></svg>", [100, 100], {}) }
29
+ describe "#points" do
30
+ before do
31
+ sizing = instance_double(Prawn::SVG::Calculators::DocumentSizing, viewport_width: 600, viewport_height: 400, viewport_diagonal: 500, :requested_width= => nil, :requested_height= => nil)
32
+ expect(sizing).to receive(:calculate)
33
+ expect(Prawn::SVG::Calculators::DocumentSizing).to receive(:new).and_return(sizing)
34
+ end
35
+
36
+ let(:document) { Prawn::SVG::Document.new("<svg></svg>", [100, 100], {}) }
11
37
 
12
- describe :points do
13
38
  it "converts a variety of measurement units to points" do
14
- document.send(:points, 32).should == 32.0
15
- document.send(:points, 32.0).should == 32.0
39
+ document.send(:points, 32).should == 32.0
40
+ document.send(:points, 32.0).should == 32.0
16
41
  document.send(:points, "32").should == 32.0
17
42
  document.send(:points, "32unknown").should == 32.0
18
- document.send(:points, "32pt").should == 32.0
43
+ document.send(:points, "32pt").should == 32.0
19
44
  document.send(:points, "32in").should == 32.0 * 72
20
45
  document.send(:points, "32ft").should == 32.0 * 72 * 12
21
46
  document.send(:points, "32pc").should == 32.0 * 15
@@ -2,9 +2,9 @@ require 'spec_helper'
2
2
 
3
3
  describe Prawn::SVG::Elements::Base do
4
4
  let(:svg) { "<svg></svg>" }
5
- let(:document) { Prawn::SVG::Document.new(svg, [800, 600], {}) }
5
+ let(:document) { Prawn::SVG::Document.new(svg, [800, 600], {}, font_registry: Prawn::SVG::FontRegistry.new("Helvetica" => {:normal => nil})) }
6
6
  let(:parent_calls) { [] }
7
- let(:element) { Prawn::SVG::Elements::Base.new(document, document.root, parent_calls, {}) }
7
+ let(:element) { Prawn::SVG::Elements::Base.new(document, document.root, parent_calls, Prawn::SVG::State.new) }
8
8
 
9
9
  describe "#initialize" do
10
10
  let(:svg) { '<something id="hello"/>' }
@@ -52,7 +52,7 @@ describe Prawn::SVG::Elements::Base do
52
52
  end
53
53
 
54
54
  element.process
55
- expect(element.parent_calls).to eq [["end_path", [], [["test", ["argument"], []]]]]
55
+ expect(element.parent_calls).to eq [["fill", [], [["test", ["argument"], []]]]]
56
56
  end
57
57
 
58
58
  it "quietly absorbs a SkipElementQuietly exception" do
@@ -79,47 +79,69 @@ describe Prawn::SVG::Elements::Base do
79
79
 
80
80
  it "doesn't change anything if no fill attribute provided" do
81
81
  subject
82
- expect(element.state[:fill]).to be nil
82
+ expect(element.state.fill).to be true
83
+
84
+ element.state.fill = false
85
+ subject
86
+ expect(element.state.fill).to be false
83
87
  end
84
88
 
85
89
  it "doesn't change anything if 'inherit' fill attribute provided" do
86
90
  element.attributes['fill'] = 'inherit'
87
91
  subject
88
- expect(element.state[:fill]).to be nil
92
+ expect(element.state.fill).to be true
93
+
94
+ element.state.fill = false
95
+ subject
96
+ expect(element.state.fill).to be false
89
97
  end
90
98
 
91
99
  it "turns off filling if 'none' fill attribute provided" do
92
100
  element.attributes['fill'] = 'none'
93
101
  subject
94
- expect(element.state[:fill]).to be false
102
+ expect(element.state.fill).to be false
95
103
  end
96
104
 
97
105
  it "uses the fill attribute's color" do
98
106
  expect(element).to receive(:add_call).with('fill_color', 'ff0000')
99
107
  element.attributes['fill'] = 'red'
100
108
  subject
101
- expect(element.state[:fill]).to be true
109
+ expect(element.state.fill).to be true
102
110
  end
103
111
 
104
112
  it "uses black if the fill attribute's color is unparseable" do
105
113
  expect(element).to receive(:add_call).with('fill_color', '000000')
106
114
  element.attributes['fill'] = 'blarble'
107
115
  subject
108
- expect(element.state[:fill]).to be true
116
+ expect(element.state.fill).to be true
109
117
  end
110
118
 
111
119
  it "uses the color attribute if 'currentColor' fill attribute provided" do
112
120
  expect(element).to receive(:add_call).with('fill_color', 'ff0000')
113
121
  element.attributes['fill'] = 'currentColor'
114
122
  element.attributes['color'] = 'red'
123
+ element.send :parse_color_attribute
115
124
  subject
116
- expect(element.state[:fill]).to be true
125
+ expect(element.state.fill).to be true
126
+ end
127
+
128
+ context "with a color attribute defined on a parent element" do
129
+ let(:svg) { '<svg style="color: green;"><g style="color: red;"><rect width="10" height="10" style="fill: currentColor;"></rect></g></svg>' }
130
+ let(:element) { Prawn::SVG::Elements::Root.new(document, document.root, parent_calls) }
131
+ let(:flattened_calls) { flatten_calls(element.base_calls) }
132
+
133
+ it "uses the parent's color element if 'currentColor' fill attribute provided" do
134
+ element.process
135
+
136
+ expect(flattened_calls).to include ['fill_color', ['ff0000']]
137
+ expect(flattened_calls).not_to include ['fill_color', ['00ff00']]
138
+ end
117
139
  end
118
140
 
119
141
  it "turns off filling if UnresolvableURLWithNoFallbackError is raised" do
120
142
  element.attributes['fill'] = 'url()'
121
143
  subject
122
- expect(element.state[:fill]).to be false
144
+ expect(element.state.fill).to be false
123
145
  end
124
146
  end
125
147
  end