prawn-svg 0.19.0 → 0.20.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dcccb8c6d35a2f33debf7f17d04dc1a8b3099398
4
- data.tar.gz: 3ac53d2c35c41f1de03582e46175a9bf296a6cd0
3
+ metadata.gz: fb790776a1eaf53a702045a42ea22fce3e8afa51
4
+ data.tar.gz: d90d2245d141bc749ec04d0b6e9239a9e2d8b48c
5
5
  SHA512:
6
- metadata.gz: ea4c6fb099669924881a72e4d983a5b890a4e74d38eb5bc7e1173223485acc35a69b0dabf95c41329bc1c254a6be53771d98f5e13fcf5d5c118c2fa1b2bd85ce
7
- data.tar.gz: d3440e0c86365ad251beec2011f1f243dfc35e24ee82af3684c925e2e9c08d4b44e416e366cbd036343908241567cfd75878929bc5ce98fa9e4f81cdde963317
6
+ metadata.gz: a0a11f3a0a93c36d838ed2bcb012cfa010f4968de481795e8f46fde61460c71c1f491586e477c24d1c55792f9293d2d52f4dfb9f28c7447cd4c3f87623e17c7f
7
+ data.tar.gz: 5885cb0acea63c8edd59fe563e4b6859f0d536537324026e8e18bb390fe1a0e335be7c2eaf07ef1d529ea752804a4fa8387a0f61d3b7ea362f76df60b13f0d5c
data/README.md CHANGED
@@ -41,7 +41,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
41
41
  implementation of elliptical arc is a bit rough at the moment.
42
42
 
43
43
  - <tt>&lt;text&gt;</tt> and <tt>&lt;tspan&gt;</tt> with attributes
44
- <tt>text-anchor</tt>, <tt>font-size</tt>, <tt>font-family</tt>, <tt>font-weight</tt>, <tt>font-style</tt>, <tt>dx</tt>, <tt>dy</tt>
44
+ <tt>text-anchor</tt>, <tt>font-size</tt>, <tt>font-family</tt>, <tt>font-weight</tt>, <tt>font-style</tt>, <tt>letter-spacing</tt>, <tt>dx</tt>, <tt>dy</tt>
45
45
 
46
46
  - <tt>&lt;svg&gt;</tt>, <tt>&lt;g&gt;</tt> and <tt>&lt;symbol&gt;</tt>
47
47
 
@@ -1,8 +1,11 @@
1
1
  module Prawn::Svg::Calculators
2
2
  class DocumentSizing
3
+ DEFAULT_WIDTH = 300
4
+ DEFAULT_HEIGHT = 150
5
+ DEFAULT_ASPECT_RATIO = "xMidYMid meet"
6
+
3
7
  attr_writer :document_width, :document_height
4
8
  attr_writer :view_box, :preserve_aspect_ratio
5
- attr_writer :requested_width, :requested_height
6
9
 
7
10
  attr_reader :bounds
8
11
  attr_reader :x_offset, :y_offset, :x_scale, :y_scale
@@ -17,59 +20,80 @@ module Prawn::Svg::Calculators
17
20
  @document_width = attributes['width']
18
21
  @document_height = attributes['height']
19
22
  @view_box = attributes['viewBox']
20
- @preserve_aspect_ratio = attributes['preserveAspectRatio']
23
+ @preserve_aspect_ratio = attributes['preserveAspectRatio'] || DEFAULT_ASPECT_RATIO
21
24
  end
22
25
 
23
26
  def calculate
24
27
  @x_offset = @y_offset = 0
25
28
  @x_scale = @y_scale = 1
26
29
 
27
- width = Prawn::Svg::Calculators::Pixels.to_pixels(@document_width, @bounds[0])
28
- height = Prawn::Svg::Calculators::Pixels.to_pixels(@document_height, @bounds[1])
29
-
30
- default_aspect_ratio = "x#{width ? "Mid" : "Min"}Y#{height ? "Mid" : "Min"} meet"
30
+ container_width = @requested_width || @bounds[0]
31
+ container_height = @requested_height || @bounds[1]
31
32
 
32
- width ||= @bounds[0]
33
- height ||= @bounds[1]
33
+ @output_width = Pixels.to_pixels(@document_width || @requested_width, container_width)
34
+ @output_height = Pixels.to_pixels(@document_height || @requested_height, container_height)
34
35
 
35
36
  if @view_box
36
- values = @view_box.strip.split(/\s+/)
37
+ values = @view_box.strip.split(Prawn::Svg::Parser::COMMA_WSP_REGEXP)
37
38
  @x_offset, @y_offset, @viewport_width, @viewport_height = values.map {|value| value.to_f}
38
39
  @x_offset = -@x_offset
39
40
 
40
- @preserve_aspect_ratio ||= default_aspect_ratio
41
- aspect = Prawn::Svg::Calculators::AspectRatio.new(@preserve_aspect_ratio, [width, height], [@viewport_width, @viewport_height])
42
- @x_scale = aspect.width / @viewport_width
43
- @y_scale = aspect.height / @viewport_height
44
- @x_offset -= aspect.x
45
- @y_offset -= aspect.y
41
+ if @viewport_width > 0 && @viewport_height > 0
42
+ @output_width ||= container_width
43
+ @output_height ||= @output_width * @viewport_height / @viewport_width
44
+
45
+ aspect = AspectRatio.new(@preserve_aspect_ratio, [@output_width, @output_height], [@viewport_width, @viewport_height])
46
+ @x_scale = aspect.width / @viewport_width
47
+ @y_scale = aspect.height / @viewport_height
48
+ @x_offset -= aspect.x / @x_scale
49
+ @y_offset -= aspect.y / @y_scale
50
+ end
51
+ else
52
+ @output_width ||= Pixels.to_pixels(DEFAULT_WIDTH, container_width)
53
+ @output_height ||= Pixels.to_pixels(DEFAULT_HEIGHT, container_height)
54
+
55
+ @viewport_width = @output_width
56
+ @viewport_height = @output_height
46
57
  end
47
58
 
48
- @viewport_width ||= width
49
- @viewport_height ||= height
59
+ return if invalid?
50
60
 
51
61
  # SVG 1.1 section 7.10
52
62
  @viewport_diagonal = Math.sqrt(@viewport_width**2 + @viewport_height**2) / Math.sqrt(2)
53
63
 
54
64
  if @requested_width
55
- scale = @requested_width / width
56
- width = @requested_width
57
- height *= scale
65
+ scale = @requested_width / @output_width
66
+ @output_width = @requested_width
67
+ @output_height *= scale
58
68
  @x_scale *= scale
59
69
  @y_scale *= scale
60
70
 
61
71
  elsif @requested_height
62
- scale = @requested_height / height
63
- height = @requested_height
64
- width *= scale
72
+ scale = @requested_height / @output_height
73
+ @output_height = @requested_height
74
+ @output_width *= scale
65
75
  @x_scale *= scale
66
76
  @y_scale *= scale
67
77
  end
68
78
 
69
- @output_width = width
70
- @output_height = height
71
-
72
79
  self
73
80
  end
81
+
82
+ def invalid?
83
+ @viewport_width <= 0 ||
84
+ @viewport_height <= 0 ||
85
+ @output_width <= 0 ||
86
+ @output_height <= 0 ||
87
+ (@requested_width && @requested_width <= 0) ||
88
+ (@requested_height && @requested_height <= 0)
89
+ end
90
+
91
+ def requested_width=(value)
92
+ @requested_width = (value.to_f if value)
93
+ end
94
+
95
+ def requested_height=(value)
96
+ @requested_height = (value.to_f if value)
97
+ end
74
98
  end
75
99
  end
@@ -49,6 +49,11 @@ module Prawn
49
49
  # Draws the SVG to the Prawn::Document object.
50
50
  #
51
51
  def draw
52
+ if @document.sizing.invalid?
53
+ @document.warnings << "Zero or negative sizing data means this SVG cannot be rendered"
54
+ return
55
+ end
56
+
52
57
  prawn.bounding_box(position, :width => @document.sizing.output_width, :height => @document.sizing.output_height) do
53
58
  prawn.save_graphics_state do
54
59
  clip_rectangle 0, 0, @document.sizing.output_width, @document.sizing.output_height
@@ -1,11 +1,11 @@
1
1
  class Prawn::Svg::Parser::Text
2
2
  def parse(element)
3
3
  element.add_call_and_enter "text_group"
4
- internal_parse(element, [element.document.x(0)], [element.document.y(0)], false)
4
+ internal_parse(element, [element.document.x(0)], [element.document.y(0)])
5
5
  end
6
6
 
7
7
  protected
8
- def internal_parse(element, x_positions, y_positions, relative)
8
+ def internal_parse(element, x_positions, y_positions, relative = false, preserve_space = false)
9
9
  return if element.state[:display] == "none"
10
10
 
11
11
  attrs = element.attributes
@@ -20,6 +20,13 @@ class Prawn::Svg::Parser::Text
20
20
  element.add_call_and_enter "translate", element.document.distance(attrs['dx'] || 0), -element.document.distance(attrs['dy'] || 0)
21
21
  end
22
22
 
23
+ case attrs['xml:space']
24
+ when 'preserve'
25
+ preserve_space = true
26
+ when 'default'
27
+ preserve_space = false
28
+ end
29
+
23
30
  opts = {}
24
31
  if size = element.state[:font_size]
25
32
  opts[:size] = size
@@ -32,9 +39,13 @@ class Prawn::Svg::Parser::Text
32
39
  opts[:text_anchor] = anchor
33
40
  end
34
41
 
42
+ if spacing = attrs['letter-spacing']
43
+ element.add_call_and_enter 'character_spacing', element.document.points(spacing)
44
+ end
45
+
35
46
  element.element.children.each do |child|
36
47
  if child.node_type == :text
37
- text = child.value.strip.gsub(/\s+/, " ")
48
+ text = child.value.strip.gsub(preserve_space ? /[\n\t]/ : /\s+/, " ")
38
49
 
39
50
  while text != ""
40
51
  opts[:at] = [x_positions.first, y_positions.first]
@@ -56,7 +67,7 @@ class Prawn::Svg::Parser::Text
56
67
  element.add_call 'save'
57
68
  child.attributes['text-anchor'] ||= opts[:text_anchor] if opts[:text_anchor]
58
69
  child_element = Prawn::Svg::Element.new(element.document, child, element.calls, element.state.dup)
59
- internal_parse(child_element, x_positions, y_positions, relative)
70
+ internal_parse(child_element, x_positions, y_positions, relative, preserve_space)
60
71
  child_element.append_calls_to_parent
61
72
  element.add_call 'restore'
62
73
 
@@ -1,5 +1,5 @@
1
1
  module Prawn
2
2
  module Svg
3
- VERSION = '0.19.0'
3
+ VERSION = '0.20.0'
4
4
  end
5
5
  end
@@ -31,7 +31,7 @@ describe Prawn::Svg::Calculators::DocumentSizing do
31
31
  describe "#calculate" do
32
32
  it "calculates the document sizing measurements for a given set of inputs" do
33
33
  sizing.calculate
34
- expect(sizing.x_offset).to eq -75
34
+ expect(sizing.x_offset).to eq -75 / 0.25
35
35
  expect(sizing.y_offset).to eq -30
36
36
  expect(sizing.x_scale).to eq 0.25
37
37
  expect(sizing.y_scale).to eq 0.25
@@ -69,5 +69,60 @@ describe Prawn::Svg::Calculators::DocumentSizing do
69
69
  expect(sizing.output_width).to eq 600
70
70
  expect(sizing.output_height).to eq 400
71
71
  end
72
+
73
+ context "when SVG does not specify width and height" do
74
+ context "when a viewBox is specified" do
75
+ let(:attributes) { {"viewBox" => "0 0 100 200"} }
76
+
77
+ it "defaults to 100% width and the width times aspect ratio in height" do
78
+ sizing.calculate
79
+ expect(sizing.viewport_width).to eq 100
80
+ expect(sizing.viewport_height).to eq 200
81
+ expect(sizing.output_width).to eq 1200
82
+ expect(sizing.output_height).to eq 2400
83
+ end
84
+ end
85
+
86
+ context "when a requested width and height are supplied" do
87
+ let(:attributes) { {} }
88
+
89
+ it "uses the requested width and height" do
90
+ sizing.requested_width = 550
91
+ sizing.requested_height = 400
92
+ sizing.calculate
93
+
94
+ expect(sizing.viewport_width).to eq 550
95
+ expect(sizing.viewport_height).to eq 400
96
+ expect(sizing.output_width).to eq 550
97
+ expect(sizing.output_height).to eq 400
98
+ end
99
+ end
100
+
101
+ context "when a viewBox and a requested width/height are supplied" do
102
+ let(:attributes) { {"viewBox" => "0 0 100 200"} }
103
+
104
+ it "uses the requested width and height" do
105
+ sizing.requested_width = 550
106
+ sizing.requested_height = 400
107
+ sizing.calculate
108
+
109
+ expect(sizing.viewport_width).to eq 100
110
+ expect(sizing.viewport_height).to eq 200
111
+ expect(sizing.output_width).to eq 550
112
+ expect(sizing.output_height).to eq 400
113
+ end
114
+ end
115
+
116
+ context "when neither viewBox nor requested width/height specified" do
117
+ let(:attributes) { {} }
118
+
119
+ it "defaults to 300x150, because that's what HTML does" do
120
+ sizing.calculate
121
+
122
+ expect(sizing.output_width).to eq 300
123
+ expect(sizing.output_height).to eq 150
124
+ end
125
+ end
126
+ end
72
127
  end
73
128
  end
@@ -21,20 +21,31 @@ describe Prawn::Svg::Interface do
21
21
  end
22
22
  end
23
23
 
24
+ describe "#draw" do
25
+ context "when the sizing object indicates the sizes are invalid" do
26
+ let(:interface) { Prawn::Svg::Interface.new('<svg width="0"></svg>', prawn, {}) }
27
+
28
+ it "doesn't draw anything and adds a warning" do
29
+ interface.draw
30
+ expect(interface.document.warnings).to eq ["Zero or negative sizing data means this SVG cannot be rendered"]
31
+ end
32
+ end
33
+ end
34
+
24
35
  describe "#position" do
36
+ subject { interface.position }
37
+
25
38
  context "when options[:at] supplied" do
26
- it "returns options[:at]" do
27
- interface = Prawn::Svg::Interface.new(svg, prawn, at: [1, 2], position: :left)
39
+ let(:interface) { Prawn::Svg::Interface.new(svg, prawn, at: [1, 2], position: :left) }
28
40
 
29
- expect(interface.position).to eq [1, 2]
41
+ it "returns options[:at]" do
42
+ expect(subject).to eq [1, 2]
30
43
  end
31
44
  end
32
45
 
33
46
  context "when only a position is supplied" do
34
47
  let(:interface) { Prawn::Svg::Interface.new(svg, prawn, position: position) }
35
48
 
36
- subject { interface.position }
37
-
38
49
  context "(:left)" do
39
50
  let(:position) { :left }
40
51
  it { is_expected.to eq [0, 600] }
@@ -55,5 +66,29 @@ describe Prawn::Svg::Interface do
55
66
  it { is_expected.to eq [25.5, 600] }
56
67
  end
57
68
  end
69
+
70
+ context "when a vposition is supplied" do
71
+ let(:interface) { Prawn::Svg::Interface.new(svg, prawn, vposition: vposition) }
72
+
73
+ context "(:top)" do
74
+ let(:vposition) { :top }
75
+ it { is_expected.to eq [0, 600] }
76
+ end
77
+
78
+ context "(:center)" do
79
+ let(:vposition) { :center }
80
+ it { is_expected.to eq [0, 350] }
81
+ end
82
+
83
+ context "(:bottom)" do
84
+ let(:vposition) { :bottom }
85
+ it { is_expected.to eq [0, 100] }
86
+ end
87
+
88
+ context "a number" do
89
+ let(:vposition) { 25.5 }
90
+ it { is_expected.to eq [0, 600 - 25.5] }
91
+ end
92
+ end
58
93
  end
59
94
  end
@@ -1,4 +1,53 @@
1
1
  require File.dirname(__FILE__) + '/../../../spec_helper'
2
2
 
3
- describe Prawn::Svg::Parser::Text do
3
+ describe Prawn::Svg::Parser::Text do
4
+ let(:document) { Prawn::Svg::Document.new(svg, [800, 600], {}) }
5
+ let(:element) { Prawn::Svg::Element.new(document, document.root, [], {}) }
6
+ let(:parser) { Prawn::Svg::Parser::Text.new }
7
+
8
+ describe "xml:space preserve" do
9
+ let(:svg) { %(<text#{attributes}>some\n\t text</text>) }
10
+
11
+ context "when xml:space is preserve" do
12
+ let(:attributes) { ' xml:space="preserve"' }
13
+
14
+ it "converts newlines and tabs to spaces, and preserves spaces" do
15
+ parser.parse(element)
16
+
17
+ expect(element.calls).to eq [
18
+ ["draw_text", ["some text", {:style=>nil, :at=>[0.0, 150.0]}], []]
19
+ ]
20
+ end
21
+ end
22
+
23
+ context "when xml:space is unspecified" do
24
+ let(:attributes) { '' }
25
+
26
+ it "strips space" do
27
+ parser.parse(element)
28
+
29
+ expect(element.calls).to eq [
30
+ ["draw_text", ["some text", {:style=>nil, :at=>[0.0, 150.0]}], []]
31
+ ]
32
+ end
33
+ end
34
+ end
35
+
36
+ describe "letter-spacing" do
37
+ let(:svg) { '<text letter-spacing="5">spaced</text>' }
38
+
39
+ it "calls character_spacing with the requested size" do
40
+ parser.parse(element)
41
+
42
+ expect(element.base_calls).to eq [
43
+ ["end_path", [], [
44
+ ["text_group", [], [
45
+ ["character_spacing", [5.0], [
46
+ ["draw_text", ["spaced", {:style=>nil, :at=>[0.0, 150.0]}], []]
47
+ ]]
48
+ ]]
49
+ ]]
50
+ ]
51
+ end
52
+ end
4
53
  end
@@ -0,0 +1,4 @@
1
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
2
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="10 10 90 90">
3
+ <rect x="10" y="10" width="90" height="90" fill="blue" stroke="red"></rect>
4
+ </svg>
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="500" height="500">
3
+ <text x="0" y="40" xml:space="preserve" font-size="30" letter-spacing="5">Text (Far away text)</text>
4
+ <text x="0" y="80" font-size="30" letter-spacing="5">Text (Close text)</text>
5
+ <text x="0" y="120" xml:space="preserve" font-size="30" letter-spacing="5">
6
+ Text
7
+ With
8
+ Newlines
9
+ And
10
+ Preserve
11
+ </text>
12
+ <text x="0" y="160" font-size="30" letter-spacing="5">
13
+ Text
14
+ With
15
+ Newlines
16
+ No
17
+ Preserve
18
+ </text>
19
+ </svg>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prawn-svg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roger Nesbitt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-09 00:00:00.000000000 Z
11
+ date: 2015-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prawn
@@ -139,12 +139,14 @@ files:
139
139
  - spec/sample_svg/matrix_transform.svg
140
140
  - spec/sample_svg/matrix_transform_3.svg
141
141
  - spec/sample_svg/negminy.svg
142
+ - spec/sample_svg/no_width_or_height.svg
142
143
  - spec/sample_svg/omnigraffle.svg
143
144
  - spec/sample_svg/opacity01.svg
144
145
  - spec/sample_svg/path.svg
145
146
  - spec/sample_svg/pie_piece.svg
146
147
  - spec/sample_svg/polygon01.svg
147
148
  - spec/sample_svg/polyline01.svg
149
+ - spec/sample_svg/preserve-space.svg
148
150
  - spec/sample_svg/quad01.svg
149
151
  - spec/sample_svg/rect01.svg
150
152
  - spec/sample_svg/rect02.svg
@@ -223,12 +225,14 @@ test_files:
223
225
  - spec/sample_svg/matrix_transform.svg
224
226
  - spec/sample_svg/matrix_transform_3.svg
225
227
  - spec/sample_svg/negminy.svg
228
+ - spec/sample_svg/no_width_or_height.svg
226
229
  - spec/sample_svg/omnigraffle.svg
227
230
  - spec/sample_svg/opacity01.svg
228
231
  - spec/sample_svg/path.svg
229
232
  - spec/sample_svg/pie_piece.svg
230
233
  - spec/sample_svg/polygon01.svg
231
234
  - spec/sample_svg/polyline01.svg
235
+ - spec/sample_svg/preserve-space.svg
232
236
  - spec/sample_svg/quad01.svg
233
237
  - spec/sample_svg/rect01.svg
234
238
  - spec/sample_svg/rect02.svg