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