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