prawn-svg 0.27.1 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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