prawn-svg 0.27.1 → 0.28.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.
@@ -0,0 +1,12 @@
1
+ class Prawn::SVG::Elements::ClipPath < Prawn::SVG::Elements::Base
2
+ def parse
3
+ state.inside_clip_path = true
4
+ properties.display = 'none'
5
+ computed_properties.display = 'none'
6
+ end
7
+
8
+ def container?
9
+ true
10
+ end
11
+ end
12
+
@@ -1,9 +1,7 @@
1
1
  class Prawn::SVG::Elements::Container < Prawn::SVG::Elements::Base
2
2
  def parse
3
- state.disable_drawing = true if name == 'clipPath'
4
-
5
3
  set_display_none if name == 'symbol' && !state.inside_use
6
- set_display_none if %w(defs clipPath).include?(name)
4
+ set_display_none if name == 'defs'
7
5
  end
8
6
 
9
7
  def container?
@@ -7,7 +7,7 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::DepthFirstBase
7
7
  # of the element, delegating it to our root text component.
8
8
 
9
9
  def parse_step
10
- state.text = Prawn::SVG::Elements::TextComponent::PositionsList.new([], [], [], [], [], nil)
10
+ state.text = Prawn::SVG::Elements::TextComponent::TextState.new
11
11
 
12
12
  @text_root = Prawn::SVG::Elements::TextComponent.new(document, source, nil, state.dup)
13
13
  @text_root.parse_step
@@ -2,14 +2,20 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
2
2
  attr_reader :commands
3
3
 
4
4
  Printable = Struct.new(:element, :text, :leading_space?, :trailing_space?)
5
- PositionsList = Struct.new(:x, :y, :dx, :dy, :rotation, :parent)
5
+ TextState = Struct.new(:parent, :x, :y, :dx, :dy, :rotation, :spacing, :mode)
6
6
 
7
7
  def parse
8
- state.text.x = attributes['x'].split(COMMA_WSP_REGEXP).collect {|n| x(n)} if attributes['x']
9
- state.text.y = attributes['y'].split(COMMA_WSP_REGEXP).collect {|n| y(n)} if attributes['y']
10
- state.text.dx = attributes['dx'].split(COMMA_WSP_REGEXP).collect {|n| x_pixels(n)} if attributes['dx']
11
- state.text.dy = attributes['dy'].split(COMMA_WSP_REGEXP).collect {|n| y_pixels(n)} if attributes['dy']
12
- state.text.rotation = attributes['rotate'].split(COMMA_WSP_REGEXP).collect(&:to_f) if attributes['rotate']
8
+ if state.inside_clip_path
9
+ raise SkipElementError, "<text> elements are not supported in clip paths"
10
+ end
11
+
12
+ state.text.x = (attributes['x'] || "").split(COMMA_WSP_REGEXP).collect { |n| x(n) }
13
+ state.text.y = (attributes['y'] || "").split(COMMA_WSP_REGEXP).collect { |n| y(n) }
14
+ state.text.dx = (attributes['dx'] || "").split(COMMA_WSP_REGEXP).collect { |n| x_pixels(n) }
15
+ state.text.dy = (attributes['dy'] || "").split(COMMA_WSP_REGEXP).collect { |n| y_pixels(n) }
16
+ state.text.rotation = (attributes['rotate'] || "").split(COMMA_WSP_REGEXP).collect(&:to_f)
17
+ state.text.spacing = calculate_character_spacing
18
+ state.text.mode = calculate_text_rendering_mode
13
19
 
14
20
  @commands = []
15
21
 
@@ -41,10 +47,13 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
41
47
  text_anchor: computed_properties.text_anchor
42
48
  }
43
49
 
44
- spacing = computed_properties.letter_spacing
45
- spacing = spacing == 'normal' ? 0 : pixels(spacing)
46
-
47
- add_call_and_enter 'character_spacing', spacing
50
+ if state.text.parent
51
+ add_call_and_enter 'character_spacing', state.text.spacing unless state.text.spacing == state.text.parent.spacing
52
+ add_call_and_enter 'text_rendering_mode', state.text.mode unless state.text.mode == state.text.parent.mode
53
+ else
54
+ add_call_and_enter 'character_spacing', state.text.spacing unless state.text.spacing == 0
55
+ add_call_and_enter 'text_rendering_mode', state.text.mode unless state.text.mode == :fill
56
+ end
48
57
 
49
58
  @commands.each do |command|
50
59
  case command
@@ -59,8 +68,8 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
59
68
  end
60
69
  end
61
70
 
62
- # It's possible there was no text to render. In that case, add a 'noop' so
63
- # character_spacing doesn't blow up when it finds it doesn't have a block to execute.
71
+ # It's possible there was no text to render. In that case, add a 'noop' so character_spacing/text_rendering_mode
72
+ # don't blow up when they find they don't have a block to execute.
64
73
  add_call 'noop' if calls.empty?
65
74
  end
66
75
 
@@ -81,7 +90,7 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
81
90
 
82
91
  def append_child(child)
83
92
  new_state = state.dup
84
- new_state.text = PositionsList.new([], [], [], [], [], state.text)
93
+ new_state.text = TextState.new(state.text)
85
94
 
86
95
  element = self.class.new(document, child, calls, new_state)
87
96
  @commands << element
@@ -189,4 +198,28 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
189
198
  def apply_font(font)
190
199
  add_call 'font', font.name, style: font.subfamily
191
200
  end
201
+
202
+ def calculate_text_rendering_mode
203
+ fill = computed_properties.fill != 'none'
204
+ stroke = computed_properties.stroke != 'none'
205
+
206
+ if fill && stroke
207
+ :fill_stroke
208
+ elsif fill
209
+ :fill
210
+ elsif stroke
211
+ :stroke
212
+ else
213
+ :invisible
214
+ end
215
+ end
216
+
217
+ def calculate_character_spacing
218
+ spacing = computed_properties.letter_spacing
219
+ spacing == 'normal' ? 0 : pixels(spacing)
220
+ end
221
+
222
+ # overridden, we don't want to call fill/stroke as draw_text does this for us
223
+ def apply_drawing_call
224
+ end
192
225
  end
@@ -24,7 +24,7 @@ class Prawn::SVG::FontRegistry
24
24
  end
25
25
 
26
26
  def load(family, weight = nil, style = nil)
27
- Prawn::SVG::CSS.parse_font_family_string(family).detect do |name|
27
+ Prawn::SVG::CSS::FontFamilyParser.parse(family).detect do |name|
28
28
  name = name.gsub(/\s{2,}/, ' ').downcase
29
29
 
30
30
  font = Prawn::SVG::Font.new(name, weight, style, font_registry: self)
@@ -37,8 +37,9 @@ class Prawn::SVG::FontRegistry
37
37
  def merge_external_fonts
38
38
  if @font_case_mapping.nil?
39
39
  self.class.load_external_fonts unless self.class.external_font_families
40
- @font_families.merge!(self.class.external_font_families)
41
-
40
+ @font_families.merge!(self.class.external_font_families) do |key, v1, v2|
41
+ v1
42
+ end
42
43
  @font_case_mapping = @font_families.keys.each.with_object({}) do |key, result|
43
44
  result[key.downcase] = key
44
45
  end
@@ -186,7 +186,9 @@ module Prawn
186
186
  # prawn (as at 2.0.1 anyway) uses 'b' for its fill_and_stroke. 'b' is 'h' (closepath) + 'B', and we
187
187
  # never want closepath to be automatically run as it stuffs up many drawing operations, such as dashes
188
188
  # and line caps, and makes paths close that we didn't ask to be closed when fill is specified.
189
- prawn.add_content 'B'
189
+ even_odd = arguments[0].is_a?(Hash) && arguments[0][:fill_rule] == :even_odd
190
+ content = even_odd ? 'B*' : 'B'
191
+ prawn.add_content content
190
192
 
191
193
  when 'noop'
192
194
  yield
@@ -18,6 +18,7 @@ class Prawn::SVG::Properties
18
18
  "display" => Config.new("inline", false, %w(inherit inline none), true),
19
19
  "fill" => Config.new("black", true, %w(inherit none currentColor)),
20
20
  "fill-opacity" => Config.new("1", true),
21
+ "fill-rule" => Config.new("nonzero", true, %w(inherit nonzero evenodd)),
21
22
  "font-family" => Config.new("sans-serif", true),
22
23
  "font-size" => Config.new("medium", true, %w(inherit xx-small x-small small medium large x-large xx-large larger smaller)),
23
24
  "font-style" => Config.new("normal", true, %w(inherit normal italic oblique), true),
@@ -1,10 +1,9 @@
1
1
  class Prawn::SVG::State
2
- attr_accessor :disable_drawing,
3
- :text, :preserve_space,
2
+ attr_accessor :text, :preserve_space,
4
3
  :fill_opacity, :stroke_opacity, :stroke_width,
5
4
  :computed_properties,
6
5
  :viewport_sizing,
7
- :inside_use
6
+ :inside_use, :inside_clip_path
8
7
 
9
8
  def initialize
10
9
  @stroke_width = 1
@@ -16,4 +15,8 @@ class Prawn::SVG::State
16
15
  def initialize_dup(other)
17
16
  @computed_properties = @computed_properties.dup
18
17
  end
18
+
19
+ def disable_drawing
20
+ inside_clip_path
21
+ end
19
22
  end
@@ -1,5 +1,5 @@
1
1
  module Prawn
2
2
  module SVG
3
- VERSION = '0.27.1'
3
+ VERSION = '0.28.0'
4
4
  end
5
5
  end
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  require File.expand_path('../lib/prawn/svg/version', __FILE__)
3
3
 
4
- spec = Gem::Specification.new do |gem|
4
+ Gem::Specification.new do |gem|
5
5
  gem.name = 'prawn-svg'
6
6
  gem.version = Prawn::SVG::VERSION
7
7
  gem.summary = "SVG renderer for Prawn PDF library"
@@ -21,7 +21,7 @@ spec = Gem::Specification.new do |gem|
21
21
  gem.required_ruby_version = '>= 2.1.0'
22
22
 
23
23
  gem.add_runtime_dependency "prawn", ">= 0.11.1", "< 3"
24
- gem.add_runtime_dependency "css_parser", "~> 1.3"
24
+ gem.add_runtime_dependency "css_parser", "~> 1.6"
25
25
  gem.add_development_dependency "rspec", "~> 3.0"
26
26
  gem.add_development_dependency "rake", "~> 10.1"
27
27
  end
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
- RSpec.describe Prawn::SVG::CSS do
4
- describe "#parse_font_family_string" do
3
+ RSpec.describe Prawn::SVG::CSS::FontFamilyParser do
4
+ describe "#parse" do
5
5
  it "correctly handles quotes and escaping" do
6
6
  tests = {
7
7
  "" => [],
@@ -17,7 +17,7 @@ RSpec.describe Prawn::SVG::CSS do
17
17
  }
18
18
 
19
19
  tests.each do |string, expected|
20
- expect(Prawn::SVG::CSS.parse_font_family_string(string)).to eq expected
20
+ expect(Prawn::SVG::CSS::FontFamilyParser.parse(string)).to eq expected
21
21
  end
22
22
  end
23
23
  end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Prawn::SVG::CSS::SelectorParser do
4
+ describe "::parse" do
5
+ it "parses a simple selector" do
6
+ expect(described_class.parse("div")).to eq [{name: "div"}]
7
+ expect(described_class.parse(".c1")).to eq [{class: ["c1"]}]
8
+ end
9
+
10
+ it "parses a complex selector" do
11
+ result = described_class.parse("div#count .c1.c2 > span.large + div~.other:first-child *:nth-child(3)")
12
+ expect(result).to eq [
13
+ {name: "div", id: ["count"]},
14
+ {combinator: :descendant, class: ["c1", "c2"]},
15
+ {combinator: :child, name: "span", class: ["large"]},
16
+ {combinator: :adjacent, name: "div"},
17
+ {combinator: :siblings, class: ["other"], pseudo_class: ["first-child"]},
18
+ {combinator: :descendant, name: "*", pseudo_class: ["nth-child(3)"]},
19
+ ]
20
+ end
21
+
22
+ it "parses attributes" do
23
+ expect(described_class.parse("[abc]")).to eq [{attribute: [["abc", nil, nil]]}]
24
+ expect(described_class.parse("[abc=123]")).to eq [{attribute: [["abc", '=', '123']]}]
25
+ expect(described_class.parse("[abc^=123]")).to eq [{attribute: [["abc", '^=', '123']]}]
26
+ expect(described_class.parse("[ abc ^= 123 ]")).to eq [{attribute: [["abc", '^=', '123']]}]
27
+ expect(described_class.parse("[abc^='123']")).to eq [{attribute: [["abc", '^=', '123']]}]
28
+ expect(described_class.parse("[abc^= '123' ]")).to eq [{attribute: [["abc", '^=', '123']]}]
29
+ expect(described_class.parse("[abc^= '123\\'456' ]")).to eq [{attribute: [["abc", '^=', '123\'456']]}]
30
+ expect(described_class.parse('[abc^= "123\\"456" ]')).to eq [{attribute: [["abc", '^=', '123"456']]}]
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,113 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Prawn::SVG::CSS::Stylesheets do
4
+ describe "typical usage" do
5
+ let(:svg) { <<-SVG }
6
+ <svg>
7
+ <style>
8
+ #inner rect { fill: #0000ff; }
9
+ #outer { fill: #220000; }
10
+ .hero > rect { fill: #00ff00; }
11
+ rect { fill: #ff0000; }
12
+ rect ~ rect { fill: #330000; }
13
+ rect + rect { fill: #440000; }
14
+
15
+ circle:first-child { fill: #550000; }
16
+ circle:nth-child(2) { fill: #660000; }
17
+ circle:last-child { fill: #770000; }
18
+
19
+ square[chocolate] { fill: #880000; }
20
+ square[abc=def] { fill: #990000; }
21
+ square[abc^=ghi] { fill: #aa0000; }
22
+ square[abc$=jkl] { fill: #bb0000; }
23
+ square[abc*=mno] { fill: #cc0000; }
24
+ square[abc~=pqr] { fill: #dd0000; }
25
+ square[abc|=stu] { fill: #ee0000; }
26
+ </style>
27
+
28
+ <rect width="1" height="1" />
29
+ <rect width="2" height="2" id="outer" />
30
+
31
+ <g class="hero large">
32
+ <rect width="3" height="3" />
33
+ <rect width="4" height="4" style="fill: #777777;" />
34
+ <rect width="5" height="5" />
35
+
36
+ <g id="inner">
37
+ <rect width="6" height="6" />
38
+ </g>
39
+
40
+ <circle width="7" />
41
+ <circle width="8" />
42
+ <circle width="9" />
43
+ </g>
44
+
45
+ <square width="10" chocolate="hi there" />
46
+ <square width="11" abc="def" />
47
+ <square width="12" abc="ghidef" />
48
+ <square width="13" abc="aghidefjkl" />
49
+ <square width="14" abc="agmnohidefjklx" />
50
+ <square width="15" abc="aeo cnj pqr" />
51
+ <square width="16" abc="eij-stu-asd" />
52
+ </svg>
53
+ SVG
54
+
55
+ it "associates styles with elements" do
56
+ result = Prawn::SVG::CSS::Stylesheets.new(CssParser::Parser.new, REXML::Document.new(svg)).load
57
+ width_and_styles = result.map { |k, v| [k.attributes["width"].to_i, v] }.sort_by(&:first)
58
+
59
+ expect(width_and_styles).to eq [
60
+ [1, [["fill", "#ff0000", false]]],
61
+ [2, [["fill", "#ff0000", false], ["fill", "#330000", false], ["fill", "#440000", false], ["fill", "#220000", false]]],
62
+ [3, [["fill", "#ff0000", false], ["fill", "#00ff00", false]]],
63
+ [4, [["fill", "#ff0000", false], ["fill", "#330000", false], ["fill", "#440000", false], ["fill", "#00ff00", false]]],
64
+ [5, [["fill", "#ff0000", false], ["fill", "#330000", false], ["fill", "#330000", false], ["fill", "#00ff00", false]]],
65
+ [6, [["fill", "#ff0000", false], ["fill", "#0000ff", false]]],
66
+ [7, [["fill", "#550000", false]]],
67
+ [8, [["fill", "#660000", false]]],
68
+ [9, [["fill", "#770000", false]]],
69
+ [10, [["fill", "#880000", false]]],
70
+ [11, [["fill", "#990000", false]]],
71
+ [12, [["fill", "#aa0000", false]]],
72
+ [13, [["fill", "#bb0000", false]]],
73
+ [14, [["fill", "#cc0000", false]]],
74
+ [15, [["fill", "#dd0000", false]]],
75
+ [16, [["fill", "#ee0000", false]]],
76
+ ]
77
+ end
78
+ end
79
+
80
+ describe "style tag parsing" do
81
+ let(:svg) do
82
+ <<-SVG
83
+ <svg>
84
+ <some-tag>
85
+ <style>a
86
+ before&gt;
87
+ x <![CDATA[ y
88
+ inside <>&gt;
89
+ k ]]> j
90
+ after
91
+ z</style>
92
+ </some-tag>
93
+
94
+ <other-tag>
95
+ <more-tag>
96
+ <style>hello</style>
97
+ </more-tag>
98
+ </other-tag>
99
+ </svg>
100
+ SVG
101
+ end
102
+
103
+ it "scans the document for style tags and adds the style information to the css parser" do
104
+ css_parser = instance_double(CssParser::Parser)
105
+
106
+ expect(css_parser).to receive(:add_block!).with("a\n before>\n x y\n inside <>&gt;\n k j\n after\nz")
107
+ expect(css_parser).to receive(:add_block!).with("hello")
108
+ allow(css_parser).to receive(:each_rule_set)
109
+
110
+ Prawn::SVG::CSS::Stylesheets.new(css_parser, REXML::Document.new(svg)).load
111
+ end
112
+ end
113
+ end
@@ -25,37 +25,4 @@ describe Prawn::SVG::Document do
25
25
  end
26
26
  end
27
27
  end
28
-
29
- describe "#parse_style_elements" do
30
- let(:svg) do
31
- <<-SVG
32
- <svg>
33
- <some-tag>
34
- <style>a
35
- before&gt;
36
- x <![CDATA[ y
37
- inside <>&gt;
38
- k ]]> j
39
- after
40
- z</style>
41
- </some-tag>
42
-
43
- <other-tag>
44
- <more-tag>
45
- <style>hello</style>
46
- </more-tag>
47
- </other-tag>
48
- </svg>
49
- SVG
50
- end
51
-
52
- it "scans the document for style tags and adds the style information to the css parser" do
53
- css_parser = instance_double(CssParser::Parser)
54
-
55
- expect(css_parser).to receive(:add_block!).with("a\n before>\n x y\n inside <>&gt;\n k j\n after\nz")
56
- expect(css_parser).to receive(:add_block!).with("hello")
57
-
58
- Prawn::SVG::Document.new(svg, bounds, options, css_parser: css_parser)
59
- end
60
- end
61
28
  end
@@ -44,6 +44,36 @@ describe Prawn::SVG::Elements::Base do
44
44
  end
45
45
  end
46
46
 
47
+ describe "fills and strokes" do
48
+ before { element.process }
49
+ subject { element.base_calls.last }
50
+
51
+ context "with neither fill nor stroke" do
52
+ let(:svg) { '<rect style="fill: none;"></rect>' }
53
+ it { is_expected.to eq ['end_path', [], []] }
54
+ end
55
+
56
+ context "with a fill only" do
57
+ let(:svg) { '<rect style="fill: black;"></rect>' }
58
+ it { is_expected.to eq ['fill', [], []] }
59
+ end
60
+
61
+ context "with a stroke only" do
62
+ let(:svg) { '<rect style="fill: none; stroke: black;"></rect>' }
63
+ it { is_expected.to eq ['stroke', [], []] }
64
+ end
65
+
66
+ context "with fill and stroke" do
67
+ let(:svg) { '<rect style="fill: black; stroke: black;"></rect>' }
68
+ it { is_expected.to eq ['fill_and_stroke', [], []] }
69
+ end
70
+
71
+ context "with fill with evenodd fill rule" do
72
+ let(:svg) { '<rect style="fill: black; fill-rule: evenodd;"></rect>' }
73
+ it { is_expected.to eq ['fill', [{fill_rule: :even_odd}], []] }
74
+ end
75
+ end
76
+
47
77
  it "appends calls to the parent element" do
48
78
  expect(element).to receive(:apply) do
49
79
  element.send :add_call, "test", "argument"
@@ -131,4 +161,30 @@ describe Prawn::SVG::Elements::Base do
131
161
  expect(element.computed_properties.fill).to eq 'none'
132
162
  end
133
163
  end
164
+
165
+ describe "stylesheets" do
166
+ let(:svg) { <<-SVG }
167
+ <svg>
168
+ <style>
169
+ .special rect { fill: green; }
170
+ rect { fill: red; }
171
+ </style>
172
+ <rect width="100" height="100"></rect>
173
+ <g class="special">
174
+ <rect width="100" height="100"></rect>
175
+ <rect width="100" height="100" style="fill: yellow;"></rect>
176
+ </g>
177
+ </svg>
178
+ SVG
179
+
180
+ it "applies stylesheet styling but style attributes take precedence" do
181
+ document = Prawn::SVG::Document.new(svg, [100, 100], {})
182
+ calls = []
183
+ element = Prawn::SVG::Elements::Root.new(document, document.root, calls)
184
+ element.process
185
+
186
+ fill_colors = calls.select { |cmd, _, _| cmd == 'fill_color' }.map { |_, args, _| args.first }
187
+ expect(fill_colors).to eq ['000000', 'ff0000', '008000', 'ffff00']
188
+ end
189
+ end
134
190
  end