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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 78e98b74bfb10e36c2de7a051662546c98ddcfa5
4
- data.tar.gz: 9fe692aebb49298a976f9436141c9bf9f4e3b750
3
+ metadata.gz: 5f5f1b932d2ff278ac37b5957a18b57d51013345
4
+ data.tar.gz: 7502fc93ebe947b1955d478292bf71c7b5dd40c0
5
5
  SHA512:
6
- metadata.gz: bca8bd9a84bb372434d0a6aac7911c13e097fbdc30e788c9dad883a922237b384cc6f9786a3992193bd46ee47e562274bbe5e7f39a7ad6eba0a9311422e40c24
7
- data.tar.gz: 67af84522eb27d03b18afa2d9f6309b4a2588f411ddf123681dd69bf34992a85c2aab62946d87ab025bb12c66df3e2189b3543c5eba4e79c4de691003206696b
6
+ metadata.gz: 770335f6cd4fbff5a30d6d6ff965ff2148e66543342de66a320583f4f24ec040d341054749c619c7a9ef1491e1e0155158ba63ea407b440406791dec60fc57f2
7
+ data.tar.gz: 8e1d9b9a00984309065c1ffc93b7695aafb97d4fbc91c3b6b76d7cc9ee4fe106365b23672f7a87fe0edc97e2612a7d7d3adfe557d5aaa1af4a0c90fb15652717
data/README.md CHANGED
@@ -81,7 +81,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
81
81
  - `<switch>` and `<foreignObject>`, although prawn-svg cannot handle any data that is not SVG so `<foreignObject>`
82
82
  tags are always ignored.
83
83
 
84
- - properties: `clip-path`, `color`, `display`, `fill-opacity`, `fill`, `opacity`, `overflow`, `stroke`, `stroke-dasharray`, `stroke-linecap`, `stroke-opacity`, `stroke-width`
84
+ - properties: `clip-path`, `color`, `display`, `fill`, `fill-opacity`, `fill-rule`, `opacity`, `overflow`, `stroke`, `stroke-dasharray`, `stroke-linecap`, `stroke-opacity`, `stroke-width`
85
85
 
86
86
  - properties on lines, polylines, polygons and paths: `marker-end`, `marker-mid`, `marker-start`
87
87
 
@@ -101,12 +101,21 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
101
101
 
102
102
  ## CSS
103
103
 
104
- prawn-svg uses the css_parser gem to parse CSS <tt>&lt;style&gt;</tt> blocks. It only handles simple tag, class or id selectors; attribute and other advanced selectors are not supported by the gem.
104
+ prawn-svg supports CSS, both in `<style>` blocks and `style` attributes.
105
+
106
+ In CSS selectors you can use element names, IDs, classes, attributes (existence, `=`, `^=`, `$=`, `*=`, `~=`, `|=`)
107
+ and all combinators (` `, `>`, `+`, `~`).
108
+ The pseudo-classes `:first-child`, `:last-child` and `:nth-child(n)` (where n is a number) also work.
109
+
110
+ Pseudo-elements and the other pseudo-classes are not supported. Specificity ordering is
111
+ implemented, but `!important` is not.
105
112
 
106
113
  ## Not supported
107
114
 
108
115
  prawn-svg does not support radial gradients, patterns or filters.
109
116
 
117
+ It does not support text in the clip area, but you can clip shapes and text by any shape.
118
+
110
119
  ## Configuration
111
120
 
112
121
  ### Fonts
@@ -21,7 +21,9 @@ require 'prawn/svg/pathable'
21
21
  require 'prawn/svg/elements'
22
22
  require 'prawn/svg/extension'
23
23
  require 'prawn/svg/interface'
24
- require 'prawn/svg/css'
24
+ require 'prawn/svg/css/font_family_parser'
25
+ require 'prawn/svg/css/selector_parser'
26
+ require 'prawn/svg/css/stylesheets'
25
27
  require 'prawn/svg/ttf'
26
28
  require 'prawn/svg/font'
27
29
  require 'prawn/svg/document'
@@ -1,6 +1,6 @@
1
- class Prawn::SVG::CSS
2
- class << self
3
- def parse_font_family_string(string)
1
+ module Prawn::SVG::CSS
2
+ class FontFamilyParser
3
+ def self.parse(string)
4
4
  in_quote = nil
5
5
  in_escape = false
6
6
  current = nil
@@ -37,4 +37,3 @@ class Prawn::SVG::CSS
37
37
  end
38
38
  end
39
39
  end
40
-
@@ -0,0 +1,174 @@
1
+ module Prawn::SVG::CSS
2
+ class SelectorParser
3
+ def self.parse(selector)
4
+ tokens = tokenise_css_selector(selector) or return
5
+
6
+ result = [{}]
7
+ part = nil
8
+
9
+ tokens.each do |token|
10
+ case token
11
+ when Modifier
12
+ part = token.type
13
+ result.last[part] ||= part == :name ? "" : []
14
+ when Identifier
15
+ return unless part
16
+ result.last[part] << token.name
17
+ when Attribute
18
+ return unless ["=", "*=", "~=", "^=", "|=", "$=", nil].include?(token.operator)
19
+ (result.last[:attribute] ||= []) << [token.key, token.operator, token.value]
20
+ when Combinator
21
+ result << {combinator: token.type}
22
+ part = nil
23
+ end
24
+ end
25
+
26
+ result
27
+ end
28
+
29
+ private
30
+
31
+ VALID_CSS_IDENTIFIER_CHAR = /[a-zA-Z0-9_\u00a0-\uffff-]/
32
+ Identifier = Struct.new(:name)
33
+ Modifier = Struct.new(:type)
34
+ Combinator = Struct.new(:type)
35
+ Attribute = Struct.new(:key, :operator, :value)
36
+
37
+ def self.tokenise_css_selector(selector)
38
+ result = []
39
+ brackets = false
40
+ attribute = false
41
+ quote = false
42
+
43
+ selector.strip.chars do |char|
44
+ if brackets
45
+ result.last.name << char
46
+ brackets = false if char == ')'
47
+ elsif attribute
48
+ case attribute
49
+ when :pre_key
50
+ if VALID_CSS_IDENTIFIER_CHAR.match(char)
51
+ result.last.key = char
52
+ attribute = :key
53
+ elsif char != " " && char != "\t"
54
+ return
55
+ end
56
+
57
+ when :key
58
+ if VALID_CSS_IDENTIFIER_CHAR.match(char)
59
+ result.last.key << char
60
+ elsif char == "]"
61
+ attribute = nil
62
+ elsif "=*~^|$".include?(char)
63
+ result.last.operator = char
64
+ attribute = :operator
65
+ elsif char == " " || char == "\t"
66
+ attribute = :pre_operator
67
+ else
68
+ return
69
+ end
70
+
71
+ when :pre_operator
72
+ if "=*~^|$".include?(char)
73
+ result.last.operator = char
74
+ attribute = :operator
75
+ elsif char != " " && char != "\t"
76
+ return
77
+ end
78
+
79
+ when :operator
80
+ if "=*~^|$".include?(char)
81
+ result.last.operator << char
82
+ elsif char == " " || char == "\t"
83
+ attribute = :pre_value
84
+ elsif char == '"' || char == "'"
85
+ result.last.value = ''
86
+ attribute = char
87
+ else
88
+ result.last.value = char
89
+ attribute = :value
90
+ end
91
+
92
+ when :pre_value
93
+ if char == '"' || char == "'"
94
+ result.last.value = ''
95
+ attribute = char
96
+ elsif char != " " && char != "\t"
97
+ result.last.value = char
98
+ attribute = :value
99
+ end
100
+
101
+ when :value
102
+ if char == "]"
103
+ result.last.value = result.last.value.rstrip
104
+ attribute = nil
105
+ else
106
+ result.last.value << char
107
+ end
108
+
109
+ when '"', "'"
110
+ if char == "\\" && !quote
111
+ quote = true
112
+ elsif char == attribute && !quote
113
+ attribute = :post_string
114
+ else
115
+ quote = false
116
+ result.last.value << char
117
+ end
118
+
119
+ when :post_string
120
+ if char == "]"
121
+ attribute = nil
122
+ elsif char != " " && char != "\t"
123
+ return
124
+ end
125
+ end
126
+
127
+ elsif VALID_CSS_IDENTIFIER_CHAR.match(char)
128
+ case result.last
129
+ when Identifier
130
+ result.last.name << char
131
+ else
132
+ result << Modifier.new(:name) if !result.last.is_a?(Modifier)
133
+ result << Identifier.new(char)
134
+ end
135
+ else
136
+ case char
137
+ when "."
138
+ result << Modifier.new(:class)
139
+ when "#"
140
+ result << Modifier.new(:id)
141
+ when ":"
142
+ result << Modifier.new(:pseudo_class)
143
+ when " ", "\t"
144
+ result << Combinator.new(:descendant) unless result.last.is_a?(Combinator)
145
+ when ">"
146
+ result.pop if result.last == Combinator.new(:descendant)
147
+ result << Combinator.new(:child)
148
+ when "+"
149
+ result.pop if result.last == Combinator.new(:descendant)
150
+ result << Combinator.new(:adjacent)
151
+ when "~"
152
+ result.pop if result.last == Combinator.new(:descendant)
153
+ result << Combinator.new(:siblings)
154
+ when "*"
155
+ return unless result.empty? || result.last.is_a?(Combinator)
156
+ result << Modifier.new(:name)
157
+ result << Identifier.new("*")
158
+ when "(" # e.g. :nth-child(3n+4)
159
+ return unless result.last.is_a?(Identifier) && result[-2] && result[-2].is_a?(Modifier) && result[-2].type == :pseudo_class
160
+ result.last.name << "("
161
+ brackets = true
162
+ when "["
163
+ result << Attribute.new
164
+ attribute = :pre_key
165
+ else
166
+ return # unsupported Combinator
167
+ end
168
+ end
169
+ end
170
+
171
+ result unless brackets || attribute
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,125 @@
1
+ module Prawn::SVG::CSS
2
+ class Stylesheets
3
+ attr_reader :css_parser, :root, :media
4
+
5
+ def initialize(css_parser, root, media = :all)
6
+ @css_parser = css_parser
7
+ @root = root
8
+ @media = media
9
+ end
10
+
11
+ def load
12
+ load_style_elements
13
+ xpath_styles = gather_xpath_styles
14
+ associate_xpath_styles_with_elements(xpath_styles)
15
+ end
16
+
17
+ private
18
+
19
+ def load_style_elements
20
+ REXML::XPath.match(root, '//style').each do |source|
21
+ data = source.texts.map(&:value).join
22
+ css_parser.add_block!(data)
23
+ end
24
+ end
25
+
26
+ def gather_xpath_styles
27
+ xpath_styles = []
28
+ order = 0
29
+
30
+ css_parser.each_rule_set(media) do |rule_set, _|
31
+ declarations = []
32
+ rule_set.each_declaration { |*data| declarations << data }
33
+
34
+ rule_set.selectors.each do |selector_text|
35
+ selector = Prawn::SVG::CSS::SelectorParser.parse(selector_text)
36
+ xpath = css_selector_to_xpath(selector)
37
+ specificity = calculate_specificity(selector)
38
+ specificity << order
39
+ order += 1
40
+
41
+ xpath_styles << [xpath, declarations, specificity]
42
+ end
43
+ end
44
+
45
+ xpath_styles.sort_by(&:last)
46
+ end
47
+
48
+ def associate_xpath_styles_with_elements(xpath_styles)
49
+ element_styles = {}
50
+
51
+ xpath_styles.each do |xpath, declarations, _|
52
+ REXML::XPath.match(root, xpath).each do |element|
53
+ (element_styles[element] ||= []).concat declarations
54
+ end
55
+ end
56
+
57
+ element_styles
58
+ end
59
+
60
+ def xpath_quote(value)
61
+ %{"#{value.gsub('\\', '\\\\').gsub('"', '\\"')}"} if value
62
+ end
63
+
64
+ def css_selector_to_xpath(selector)
65
+ selector.map do |element|
66
+ pseudo_classes = Set.new(element[:pseudo_class])
67
+
68
+ result = case element[:combinator]
69
+ when :child
70
+ '/'
71
+ when :adjacent
72
+ pseudo_classes << 'first-child'
73
+ '/following-sibling::'
74
+ when :siblings
75
+ '/following-sibling::'
76
+ else
77
+ '//'
78
+ end
79
+
80
+ result << element[:name] if element[:name]
81
+ result << ((element[:class] || []).map { |name| "[contains(concat(' ',@class,' '), ' #{name} ')]" }.join)
82
+ result << ((element[:id] || []).map { |name| "[@id='#{name}']" }.join)
83
+
84
+ (element[:attribute] || []).each do |key, operator, value|
85
+ case operator
86
+ when nil
87
+ result << "[@#{key}]"
88
+ when "="
89
+ result << "[@#{key}=#{xpath_quote value}]"
90
+ when "^="
91
+ result << "[starts-with(@#{key}, #{xpath_quote value})]"
92
+ when "$="
93
+ result << "[substring(@#{key}, string-length(@#{key}) - #{value.length - 1}) = #{xpath_quote value})]"
94
+ when "*="
95
+ result << "[contains(@#{key}, #{xpath_quote value})]"
96
+ when "~="
97
+ result << "[contains(concat(' ',@#{key},' '), #{xpath_quote " #{value} "})]"
98
+ when "|="
99
+ result << "[contains(concat('-',@#{key},'-'), #{xpath_quote "-#{value}-"})]"
100
+ end
101
+ end
102
+
103
+ pseudo_classes.each do |pc|
104
+ case pc
105
+ when "first-child" then result << "[1]"
106
+ when "last-child" then result << "[last()]"
107
+ when /^nth-child\((\d+)\)$/ then result << "[#{$1}]"
108
+ end
109
+ end
110
+
111
+ result
112
+ end.join
113
+ end
114
+
115
+ def calculate_specificity(selector)
116
+ selector.reduce([0, 0, 0]) do |(a, b, c), element|
117
+ [
118
+ a + (element[:id] || []).length,
119
+ b + (element[:class] || []).length + (element[:attribute] || []).length + (element[:pseudo_class] || []).length,
120
+ c + (element[:name] && element[:name] != "*" ? 1 : 0)
121
+ ]
122
+ end
123
+ end
124
+ end
125
+ end
@@ -12,7 +12,8 @@ class Prawn::SVG::Document
12
12
  :fallback_font_name,
13
13
  :font_registry,
14
14
  :url_loader,
15
- :css_parser, :elements_by_id, :gradients
15
+ :elements_by_id, :gradients,
16
+ :element_styles
16
17
 
17
18
  def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new)
18
19
  @root = REXML::Document.new(data).root
@@ -31,7 +32,6 @@ class Prawn::SVG::Document
31
32
  @gradients = {}
32
33
  @fallback_font_name = options.fetch(:fallback_font_name, DEFAULT_FALLBACK_FONT_NAME)
33
34
  @font_registry = font_registry
34
- @css_parser = css_parser
35
35
 
36
36
  @url_loader = Prawn::SVG::UrlLoader.new(
37
37
  enable_cache: options[:cache_images],
@@ -42,7 +42,7 @@ class Prawn::SVG::Document
42
42
  @sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, @root.attributes)
43
43
  calculate_sizing(requested_width: options[:width], requested_height: options[:height])
44
44
 
45
- parse_style_elements
45
+ @element_styles = Prawn::SVG::CSS::Stylesheets.new(css_parser, root).load
46
46
 
47
47
  yield self if block_given?
48
48
  end
@@ -52,16 +52,4 @@ class Prawn::SVG::Document
52
52
  sizing.requested_height = requested_height
53
53
  sizing.calculate
54
54
  end
55
-
56
- private
57
-
58
- # <style> elements specified anywhere in the document apply to the entire
59
- # document. Because of this, we load all <style> elements before parsing
60
- # the rest of the document.
61
- def parse_style_elements
62
- REXML::XPath.match(root, '//style').each do |source|
63
- data = source.texts.map(&:value).join
64
- css_parser.add_block!(data)
65
- end
66
- end
67
55
  end
@@ -4,7 +4,7 @@ end
4
4
 
5
5
  require 'prawn/svg/elements/call_duplicator'
6
6
 
7
- %w(base depth_first_base root container viewport text text_component line polyline polygon circle ellipse rect path use image gradient marker ignored).each do |filename|
7
+ %w(base depth_first_base root container clip_path viewport text text_component line polyline polygon circle ellipse rect path use image gradient marker ignored).each do |filename|
8
8
  require "prawn/svg/elements/#{filename}"
9
9
  end
10
10
 
@@ -13,7 +13,7 @@ module Prawn::SVG::Elements
13
13
  g: Prawn::SVG::Elements::Container,
14
14
  symbol: Prawn::SVG::Elements::Container,
15
15
  defs: Prawn::SVG::Elements::Container,
16
- clipPath: Prawn::SVG::Elements::Container,
16
+ clipPath: Prawn::SVG::Elements::ClipPath,
17
17
  switch: Prawn::SVG::Elements::Container,
18
18
  svg: Prawn::SVG::Elements::Viewport,
19
19
  text: Prawn::SVG::Elements::Text,
@@ -157,14 +157,23 @@ class Prawn::SVG::Elements::Base
157
157
  end
158
158
 
159
159
  def apply_drawing_call
160
- if !state.disable_drawing && drawable?
161
- draw_types = PAINT_TYPES.select { |property| computed_properties.send(property) != 'none' }
160
+ return if state.disable_drawing || !drawable?
162
161
 
163
- if draw_types.empty?
164
- add_call_and_enter("end_path")
162
+ fill = computed_properties.fill != 'none'
163
+ stroke = computed_properties.stroke != 'none'
164
+
165
+ if fill
166
+ command = stroke ? 'fill_and_stroke' : 'fill'
167
+
168
+ if computed_properties.fill_rule == 'evenodd'
169
+ add_call_and_enter(command, {fill_rule: :even_odd})
165
170
  else
166
- add_call_and_enter(draw_types.join("_and_"))
171
+ add_call_and_enter(command)
167
172
  end
173
+ elsif stroke
174
+ add_call_and_enter('stroke')
175
+ else
176
+ add_call_and_enter('end_path')
168
177
  end
169
178
  end
170
179
 
@@ -207,32 +216,20 @@ class Prawn::SVG::Elements::Base
207
216
  end
208
217
 
209
218
  def extract_attributes_and_properties
210
- if @document && @document.css_parser
211
- tag_style = @document.css_parser.find_by_selector(source.name)
212
- id_style = @document.css_parser.find_by_selector("##{source.attributes["id"]}") if source.attributes["id"]
213
-
214
- if classes = source.attributes["class"]
215
- class_styles = classes.split(' ').collect do |class_name|
216
- @document.css_parser.find_by_selector(".#{class_name}")
217
- end
219
+ if styles = document.element_styles[source]
220
+ # TODO : implement !important, at the moment it's just ignored
221
+ styles.each do |name, value, _important|
222
+ @properties.set(name, value)
218
223
  end
219
-
220
- element_style = source.attributes['style']
221
-
222
- style = [tag_style, class_styles, id_style, element_style].flatten.collect do |s|
223
- s.nil? || s.strip == "" ? "" : "#{s}#{";" unless s.match(/;\s*\z/)}"
224
- end.join
225
- else
226
- style = source.attributes['style'] || ""
227
224
  end
228
225
 
226
+ @properties.load_hash(parse_css_declarations(source.attributes['style'] || ''))
227
+
229
228
  source.attributes.each do |name, value|
230
229
  # Properties#set returns nil if it's not a recognised property name
231
230
  @properties.set(name, value) or @attributes[name] = value
232
231
  end
233
232
 
234
- @properties.load_hash(parse_css_declarations(style))
235
-
236
233
  state.computed_properties.compute_properties(@properties)
237
234
  end
238
235