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.
- 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
@@ -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
|
data/lib/prawn/svg/url_loader.rb
CHANGED
@@ -1,34 +1,46 @@
|
|
1
|
-
require 'open-uri'
|
2
|
-
require 'base64'
|
3
|
-
|
4
1
|
class Prawn::SVG::UrlLoader
|
5
|
-
|
6
|
-
attr_reader :url_cache
|
2
|
+
Error = Class.new(StandardError)
|
7
3
|
|
8
|
-
|
9
|
-
URL_REGEXP = /^https?:\/\/|#{DATAURL_REGEXP}/
|
4
|
+
attr_reader :enable_cache, :loaders
|
10
5
|
|
11
|
-
def initialize(
|
6
|
+
def initialize(enable_cache: false, enable_web: true, enable_file_with_root: nil)
|
12
7
|
@url_cache = {}
|
13
|
-
@enable_cache =
|
14
|
-
@enable_web = opts.fetch(:enable_web, true)
|
15
|
-
end
|
8
|
+
@enable_cache = enable_cache
|
16
9
|
|
17
|
-
|
18
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
data/lib/prawn/svg/version.rb
CHANGED
data/spec/integration_spec.rb
CHANGED
@@ -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
|
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.
|
107
|
-
doc.url_loader.
|
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(:
|
16
|
-
let(:
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
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 [["
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
144
|
+
expect(element.state.fill).to be false
|
123
145
|
end
|
124
146
|
end
|
125
147
|
end
|