prawnx 0.1.1

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.
Files changed (160) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +339 -0
  4. data/GPLv3 +674 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-prawn-svg +21 -0
  7. data/README.md +107 -0
  8. data/data/fonts/Courier-Bold.afm +342 -0
  9. data/data/fonts/Courier-BoldOblique.afm +342 -0
  10. data/data/fonts/Courier-Oblique.afm +342 -0
  11. data/data/fonts/Courier.afm +342 -0
  12. data/data/fonts/Helvetica-Bold.afm +2827 -0
  13. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  14. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  15. data/data/fonts/Helvetica.afm +3051 -0
  16. data/data/fonts/MustRead.html +19 -0
  17. data/data/fonts/Symbol.afm +213 -0
  18. data/data/fonts/Times-Bold.afm +2588 -0
  19. data/data/fonts/Times-BoldItalic.afm +2384 -0
  20. data/data/fonts/Times-Italic.afm +2667 -0
  21. data/data/fonts/Times-Roman.afm +2419 -0
  22. data/data/fonts/ZapfDingbats.afm +225 -0
  23. data/lib/pdf/core.rb +4 -0
  24. data/lib/prawn/measurement_extensions.rb +4 -0
  25. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  26. data/lib/prawn/svg/attributes/mask.rb +16 -0
  27. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  28. data/lib/prawn/svg/attributes/space.rb +10 -0
  29. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  30. data/lib/prawn/svg/attributes/transform.rb +10 -0
  31. data/lib/prawn/svg/attributes.rb +6 -0
  32. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  33. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  34. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  35. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  36. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  37. data/lib/prawn/svg/color.rb +240 -0
  38. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  39. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  40. data/lib/prawn/svg/css/font_parser.rb +46 -0
  41. data/lib/prawn/svg/css/selector_parser.rb +178 -0
  42. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  43. data/lib/prawn/svg/css/values_parser.rb +68 -0
  44. data/lib/prawn/svg/document.rb +170 -0
  45. data/lib/prawn/svg/elements/anchor.rb +10 -0
  46. data/lib/prawn/svg/elements/base.rb +312 -0
  47. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  48. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  49. data/lib/prawn/svg/elements/circle.rb +25 -0
  50. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  51. data/lib/prawn/svg/elements/container.rb +10 -0
  52. data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
  53. data/lib/prawn/svg/elements/ellipse.rb +22 -0
  54. data/lib/prawn/svg/elements/gradient.rb +184 -0
  55. data/lib/prawn/svg/elements/ignored.rb +5 -0
  56. data/lib/prawn/svg/elements/image.rb +177 -0
  57. data/lib/prawn/svg/elements/line.rb +27 -0
  58. data/lib/prawn/svg/elements/marker.rb +72 -0
  59. data/lib/prawn/svg/elements/mask.rb +65 -0
  60. data/lib/prawn/svg/elements/path.rb +294 -0
  61. data/lib/prawn/svg/elements/pattern.rb +150 -0
  62. data/lib/prawn/svg/elements/polygon.rb +25 -0
  63. data/lib/prawn/svg/elements/polyline.rb +23 -0
  64. data/lib/prawn/svg/elements/rect.rb +64 -0
  65. data/lib/prawn/svg/elements/root.rb +22 -0
  66. data/lib/prawn/svg/elements/switch.rb +92 -0
  67. data/lib/prawn/svg/elements/text.rb +99 -0
  68. data/lib/prawn/svg/elements/text_component.rb +346 -0
  69. data/lib/prawn/svg/elements/text_node.rb +308 -0
  70. data/lib/prawn/svg/elements/text_path.rb +153 -0
  71. data/lib/prawn/svg/elements/use.rb +142 -0
  72. data/lib/prawn/svg/elements/viewport.rb +29 -0
  73. data/lib/prawn/svg/elements.rb +46 -0
  74. data/lib/prawn/svg/extension.rb +25 -0
  75. data/lib/prawn/svg/font.rb +20 -0
  76. data/lib/prawn/svg/font_metrics.rb +144 -0
  77. data/lib/prawn/svg/font_registry.rb +315 -0
  78. data/lib/prawn/svg/funciri.rb +14 -0
  79. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  80. data/lib/prawn/svg/gradients.rb +53 -0
  81. data/lib/prawn/svg/interface.rb +72 -0
  82. data/lib/prawn/svg/length.rb +43 -0
  83. data/lib/prawn/svg/link_renderer.rb +40 -0
  84. data/lib/prawn/svg/loaders/data.rb +17 -0
  85. data/lib/prawn/svg/loaders/file.rb +111 -0
  86. data/lib/prawn/svg/loaders/web.rb +24 -0
  87. data/lib/prawn/svg/paint.rb +67 -0
  88. data/lib/prawn/svg/pathable.rb +153 -0
  89. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  90. data/lib/prawn/svg/pdfmatrix.rb +37 -0
  91. data/lib/prawn/svg/percentage.rb +24 -0
  92. data/lib/prawn/svg/properties.rb +329 -0
  93. data/lib/prawn/svg/renderer.rb +249 -0
  94. data/lib/prawn/svg/state.rb +24 -0
  95. data/lib/prawn/svg/transform_parser.rb +78 -0
  96. data/lib/prawn/svg/ttc.rb +35 -0
  97. data/lib/prawn/svg/ttf.rb +95 -0
  98. data/lib/prawn/svg/url_loader.rb +46 -0
  99. data/lib/prawn/svg/version.rb +5 -0
  100. data/lib/prawn/table.rb +4 -0
  101. data/lib/prawn-svg.rb +53 -0
  102. data/lib/prawn.rb +10 -0
  103. data/lib/prawnx/annotations.rb +21 -0
  104. data/lib/prawnx/document/bounding_box.rb +193 -0
  105. data/lib/prawnx/document/column_box.rb +91 -0
  106. data/lib/prawnx/document/internals.rb +144 -0
  107. data/lib/prawnx/document/span.rb +25 -0
  108. data/lib/prawnx/document.rb +253 -0
  109. data/lib/prawnx/encoding.rb +29 -0
  110. data/lib/prawnx/errors.rb +37 -0
  111. data/lib/prawnx/font.rb +214 -0
  112. data/lib/prawnx/fonts/afm.rb +171 -0
  113. data/lib/prawnx/fonts/otf.rb +50 -0
  114. data/lib/prawnx/fonts/ttf.rb +410 -0
  115. data/lib/prawnx/graphic_state.rb +41 -0
  116. data/lib/prawnx/graphics/color.rb +148 -0
  117. data/lib/prawnx/graphics/patterns.rb +100 -0
  118. data/lib/prawnx/graphics/style.rb +171 -0
  119. data/lib/prawnx/graphics.rb +208 -0
  120. data/lib/prawnx/grid.rb +216 -0
  121. data/lib/prawnx/images/image.rb +244 -0
  122. data/lib/prawnx/images/png_decoder.rb +65 -0
  123. data/lib/prawnx/images.rb +115 -0
  124. data/lib/prawnx/measurement_extensions.rb +40 -0
  125. data/lib/prawnx/measurements.rb +62 -0
  126. data/lib/prawnx/outline.rb +146 -0
  127. data/lib/prawnx/page.rb +125 -0
  128. data/lib/prawnx/page_geometry.rb +27 -0
  129. data/lib/prawnx/pdf/outline_node.rb +53 -0
  130. data/lib/prawnx/pdf/reference.rb +31 -0
  131. data/lib/prawnx/pdf/writer.rb +105 -0
  132. data/lib/prawnx/pdf.rb +66 -0
  133. data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
  134. data/lib/prawnx/pdf_a.rb +130 -0
  135. data/lib/prawnx/pdf_core.rb +25 -0
  136. data/lib/prawnx/repeater.rb +79 -0
  137. data/lib/prawnx/security.rb +108 -0
  138. data/lib/prawnx/stamp.rb +74 -0
  139. data/lib/prawnx/table/cell/kinds.rb +246 -0
  140. data/lib/prawnx/table/cell.rb +331 -0
  141. data/lib/prawnx/table/cells.rb +208 -0
  142. data/lib/prawnx/table.rb +296 -0
  143. data/lib/prawnx/text/box.rb +16 -0
  144. data/lib/prawnx/text/formatted/arranger.rb +217 -0
  145. data/lib/prawnx/text/formatted/box.rb +312 -0
  146. data/lib/prawnx/text/formatted/fragment.rb +205 -0
  147. data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
  148. data/lib/prawnx/text/formatted/parser.rb +149 -0
  149. data/lib/prawnx/text/formatted/wrap.rb +103 -0
  150. data/lib/prawnx/text.rb +281 -0
  151. data/lib/prawnx/transformation_stack.rb +43 -0
  152. data/lib/prawnx/true_type/cff_subset.rb +157 -0
  153. data/lib/prawnx/true_type/file.rb +371 -0
  154. data/lib/prawnx/true_type/open_type_layout.rb +189 -0
  155. data/lib/prawnx/true_type/simple_subsets.rb +94 -0
  156. data/lib/prawnx/true_type/subset.rb +187 -0
  157. data/lib/prawnx/version.rb +7 -0
  158. data/lib/prawnx/view.rb +28 -0
  159. data/lib/prawnx.rb +102 -0
  160. metadata +251 -0
@@ -0,0 +1,178 @@
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
+
17
+ result.last[part] << token.name
18
+ when Attribute
19
+ return unless ['=', '*=', '~=', '^=', '|=', '$=', nil].include?(token.operator)
20
+
21
+ (result.last[:attribute] ||= []) << [token.key, token.operator, token.value]
22
+ when Combinator
23
+ result << { combinator: token.type }
24
+ part = nil
25
+ end
26
+ end
27
+
28
+ result
29
+ end
30
+
31
+ VALID_CSS_IDENTIFIER_CHAR = /[a-zA-Z0-9_\u00a0-\uffff-]/.freeze
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 = String.new(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 = String.new(char)
64
+ attribute = :operator
65
+ elsif [' ', "\t"].include?(char)
66
+ attribute = :pre_operator
67
+ else
68
+ return
69
+ end
70
+
71
+ when :pre_operator
72
+ if '=*~^|$'.include?(char)
73
+ result.last.operator = String.new(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 [' ', "\t"].include?(char)
83
+ attribute = :pre_value
84
+ elsif ['"', "'"].include?(char)
85
+ result.last.value = +''
86
+ attribute = String.new(char)
87
+ else
88
+ result.last.value = String.new(char)
89
+ attribute = :value
90
+ end
91
+
92
+ when :pre_value
93
+ if ['"', "'"].include?(char)
94
+ result.last.value = +''
95
+ attribute = String.new(char)
96
+ elsif char != ' ' && char != "\t"
97
+ result.last.value = String.new(char)
98
+ attribute = :value
99
+ end
100
+
101
+ when :value
102
+ if char == ']'
103
+ result.last.value = String.new(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) unless 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
+
157
+ result << Modifier.new(:name)
158
+ result << Identifier.new('*')
159
+ when '(' # e.g. :nth-child(3n+4)
160
+ unless result.last.is_a?(Identifier) && result[-2] && result[-2].is_a?(Modifier) && result[-2].type == :pseudo_class
161
+ return
162
+ end
163
+
164
+ result.last.name << '('
165
+ brackets = true
166
+ when '['
167
+ result << Attribute.new
168
+ attribute = :pre_key
169
+ else
170
+ return # unsupported Combinator
171
+ end
172
+ end
173
+ end
174
+
175
+ result unless brackets || attribute
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,233 @@
1
+ module Prawn::SVG::CSS
2
+ class Stylesheets
3
+ attr_reader :css_parser, :root, :media, :font_face_rules
4
+
5
+ def initialize(css_parser, root, media = :all, url_loader: nil, warnings: [])
6
+ @css_parser = css_parser
7
+ @root = root
8
+ @media = media
9
+ @warnings = warnings
10
+ @font_face_rules = []
11
+
12
+ @has_url_loader = !url_loader.nil?
13
+ install_url_loader_on_css_parser(url_loader) if url_loader
14
+ end
15
+
16
+ def load
17
+ load_style_elements
18
+ xpath_styles = gather_xpath_styles
19
+ associate_xpath_styles_with_elements(xpath_styles)
20
+ end
21
+
22
+ private
23
+
24
+ def install_url_loader_on_css_parser(url_loader)
25
+ warnings = @warnings
26
+
27
+ css_parser.define_singleton_method(:load_uri!) do |uri, options = {}, _deprecated = nil|
28
+ uri = Addressable::URI.parse(uri) unless uri.respond_to?(:scheme)
29
+ import_url = uri.to_s
30
+
31
+ begin
32
+ return unless circular_reference_check(import_url)
33
+
34
+ src = url_loader.load(import_url)
35
+ add_block!(src, options) if src
36
+ rescue CssParser::CircularReferenceError
37
+ # Silently ignore circular @import references
38
+ rescue Prawn::SVG::UrlLoader::Error => e
39
+ warnings << "Failed to load @import CSS from #{import_url}: #{e.message}"
40
+ end
41
+ end
42
+ end
43
+
44
+ def load_style_elements
45
+ REXML::XPath.match(root, '//style').each do |source|
46
+ data = source.texts.map(&:value).join
47
+
48
+ if @has_url_loader
49
+ css_parser.add_block!(data, base_uri: '')
50
+ else
51
+ css_parser.add_block!(data)
52
+ end
53
+ end
54
+ end
55
+
56
+ def gather_xpath_styles
57
+ xpath_styles = []
58
+ order = 0
59
+
60
+ css_parser.each_rule_set(media) do |rule_set, _|
61
+ declarations = []
62
+ rule_set.each_declaration { |*data| declarations << data }
63
+
64
+ rule_set.selectors.each do |selector_text|
65
+ if selector_text.strip == '@font-face'
66
+ @font_face_rules << declarations
67
+ next
68
+ end
69
+
70
+ next unless (selector = Prawn::SVG::CSS::SelectorParser.parse(selector_text))
71
+
72
+ xpath = css_selector_to_xpath(selector)
73
+ specificity = calculate_specificity(selector)
74
+ specificity << order
75
+ order += 1
76
+
77
+ xpath_styles << [xpath, declarations, specificity]
78
+ end
79
+ end
80
+
81
+ xpath_styles.sort_by(&:last)
82
+ end
83
+
84
+ def associate_xpath_styles_with_elements(xpath_styles)
85
+ element_styles = {}
86
+
87
+ xpath_styles.each do |xpath, declarations, _|
88
+ REXML::XPath.match(root, xpath).each do |element|
89
+ (element_styles[element] ||= []).concat declarations
90
+ end
91
+ end
92
+
93
+ element_styles
94
+ end
95
+
96
+ def xpath_quote(value)
97
+ %("#{value.gsub('\\', '\\\\').gsub('"', '\\"')}") if value
98
+ end
99
+
100
+ RECOGNIZED_PSEUDO_CLASSES = %w[first-child last-child link visited hover active focus].freeze
101
+
102
+ def css_selector_to_xpath(selector)
103
+ selector.map do |element|
104
+ pseudo_classes = Set.new(element[:pseudo_class])
105
+ require_function_name = false
106
+ never_match = false
107
+
108
+ result = case element[:combinator]
109
+ when :child
110
+ +'/'
111
+ when :adjacent
112
+ pseudo_classes << 'first-child'
113
+ +'/following-sibling::'
114
+ when :siblings
115
+ +'/following-sibling::'
116
+ else
117
+ +'//'
118
+ end
119
+
120
+ positions = []
121
+ lang_predicates = []
122
+ href_predicates = []
123
+ pseudo_classes.each do |pc|
124
+ case pc
125
+ when 'first-child' then positions << '1'
126
+ when 'last-child' then positions << 'last()'
127
+ when /^nth-child\((.+)\)$/ then positions << parse_nth_child_expression($1)
128
+ when /^lang\((.+)\)$/ then lang_predicates << $1
129
+ when 'link' then href_predicates << true
130
+ when 'visited', 'hover', 'active', 'focus' then never_match = true
131
+ else
132
+ never_match = true unless RECOGNIZED_PSEUDO_CLASSES.include?(pc)
133
+ end
134
+ end
135
+
136
+ unless positions.empty?
137
+ result << '*' unless require_function_name
138
+ require_function_name = true
139
+
140
+ logic = if positions.length == 1
141
+ positions.first
142
+ else
143
+ positions.map { |position| "position()=#{position}" }.join(' and ')
144
+ end
145
+
146
+ result << "[#{logic}]"
147
+ end
148
+
149
+ if require_function_name
150
+ result << "[name()=#{xpath_quote element[:name]}]" if element[:name]
151
+ else
152
+ result << (element[:name] || '*')
153
+ end
154
+
155
+ result << ((element[:class] || []).map { |name| "[contains(concat(' ',@class,' '), ' #{name} ')]" }.join)
156
+ result << ((element[:id] || []).map { |name| "[@id='#{name}']" }.join)
157
+
158
+ (element[:attribute] || []).each do |key, operator, value|
159
+ case operator
160
+ when nil
161
+ result << "[@#{key}]"
162
+ when '='
163
+ result << "[@#{key}=#{xpath_quote value}]"
164
+ when '^='
165
+ result << "[starts-with(@#{key}, #{xpath_quote value})]"
166
+ when '$='
167
+ result << "[substring(@#{key}, string-length(@#{key}) - #{value.length - 1}) = #{xpath_quote value}]"
168
+ when '*='
169
+ result << "[contains(@#{key}, #{xpath_quote value})]"
170
+ when '~='
171
+ result << "[contains(concat(' ',@#{key},' '), #{xpath_quote " #{value} "})]"
172
+ when '|='
173
+ result << "[contains(concat('-',@#{key},'-'), #{xpath_quote "-#{value}-"})]"
174
+ end
175
+ end
176
+
177
+ lang_predicates.each do |lang|
178
+ result << "[lang(#{xpath_quote lang})]"
179
+ end
180
+
181
+ result << '[@href or @xlink:href]' unless href_predicates.empty?
182
+
183
+ result << '[false()]' if never_match
184
+
185
+ result
186
+ end.join
187
+ end
188
+
189
+ def parse_nth_child_expression(expr)
190
+ expr = expr.strip
191
+
192
+ return '(position() mod 2 = 1)' if expr == 'odd'
193
+ return '(position() mod 2 = 0)' if expr == 'even'
194
+ return expr if expr.match?(/\A\d+\z/)
195
+
196
+ match = expr.match(/\A(-?\d*)n\s*([+-]\s*\d+)?\z/)
197
+ return 'false()' unless match
198
+
199
+ a = case match[1]
200
+ when '', '+' then 1
201
+ when '-' then -1
202
+ else match[1].to_i
203
+ end
204
+ b = match[2] ? match[2].gsub(/\s/, '').to_i : 0
205
+
206
+ if a.zero?
207
+ b.to_s
208
+ elsif a.positive?
209
+ if b <= 0
210
+ "(position() mod #{a} = #{b % a} and position() >= #{[b, 1].max})"
211
+ else
212
+ "(position() mod #{a} = #{b % a} and position() >= #{b})"
213
+ end
214
+ elsif b <= 0
215
+ # a is negative: matches positions b, b+a, b+2a, ... while > 0
216
+ # i.e. position() <= b and (position() - b) mod |a| = 0
217
+ 'false()'
218
+ else
219
+ "((position() - #{b}) mod #{a.abs} = 0 and position() >= 1 and position() <= #{b})"
220
+ end
221
+ end
222
+
223
+ def calculate_specificity(selector)
224
+ selector.reduce([0, 0, 0]) do |(a, b, c), element|
225
+ [
226
+ a + (element[:id] || []).length,
227
+ b + (element[:class] || []).length + (element[:attribute] || []).length + (element[:pseudo_class] || []).length,
228
+ c + (element[:name] && element[:name] != '*' ? 1 : 0)
229
+ ]
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,68 @@
1
+ module Prawn::SVG::CSS
2
+ class ValuesParser
3
+ class << self
4
+ def parse(values)
5
+ result = []
6
+
7
+ while values
8
+ value, remainder = parse_next(values)
9
+ break unless value
10
+
11
+ result << value
12
+ values = remainder
13
+ end
14
+
15
+ result
16
+ end
17
+
18
+ private
19
+
20
+ def parse_next(values)
21
+ values = values.strip
22
+ return if values.empty?
23
+
24
+ if (matches = values.match(/\A([a-z-]+)\(\s*(.+)/i))
25
+ parse_function_call(matches[1].downcase, matches[2])
26
+ else
27
+ values.split(/\s+/, 2)
28
+ end
29
+ end
30
+
31
+ # Note this does not support space-separated arguments.
32
+ # I don't think CSS 2 has any, but in case it does here is the place to add them.
33
+ def parse_function_call(name, rest)
34
+ arguments = []
35
+ in_quote = nil
36
+ in_escape = false
37
+ current = +''
38
+
39
+ rest.each_char.with_index do |char, index|
40
+ if in_escape
41
+ current << char
42
+ in_escape = false
43
+ elsif %w[" '].include?(char)
44
+ if in_quote == char
45
+ in_quote = nil
46
+ elsif in_quote.nil?
47
+ in_quote = char
48
+ else
49
+ current << char
50
+ end
51
+ elsif char == '\\'
52
+ in_escape = true
53
+ elsif in_quote.nil? && char == ','
54
+ arguments << current.strip
55
+ current = +''
56
+ elsif in_quote.nil? && char == ')'
57
+ arguments << current.strip
58
+ return [[name, arguments], rest[index + 1..]]
59
+ else
60
+ current << char
61
+ end
62
+ end
63
+
64
+ [rest, nil]
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,170 @@
1
+ require 'tempfile'
2
+
3
+ class Prawn::SVG::Document
4
+ Error = Class.new(StandardError)
5
+ InvalidSVGData = Class.new(Error)
6
+
7
+ DEFAULT_FALLBACK_FONT_NAME = 'Times-Roman'.freeze
8
+
9
+ class << self
10
+ attr_accessor :enable_web_requests_warned
11
+ end
12
+
13
+ # An +Array+ of warnings that occurred while parsing the SVG data.
14
+ attr_reader :warnings
15
+
16
+ attr_reader :root,
17
+ :sizing,
18
+ :fallback_font_name,
19
+ :font_registry,
20
+ :url_loader,
21
+ :elements_by_id, :gradients,
22
+ :element_styles,
23
+ :color_mode,
24
+ :external_svg_cache
25
+
26
+ def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new, attribute_overrides: {})
27
+ begin
28
+ @root = REXML::Document.new(data).root or raise_parse_error(data)
29
+ rescue REXML::ParseException => e
30
+ raise_parse_error(data, e.message)
31
+ end
32
+
33
+ @warnings = []
34
+ @options = options
35
+ @elements_by_id = {}
36
+ @gradients = Prawn::SVG::Gradients.new(self)
37
+ @external_svg_cache = {}
38
+ @fallback_font_name = options.fetch(:fallback_font_name, DEFAULT_FALLBACK_FONT_NAME)
39
+ @font_registry = font_registry
40
+ @color_mode = load_color_mode
41
+ @font_face_tempfiles = []
42
+
43
+
44
+ @url_loader = Prawn::SVG::UrlLoader.new(
45
+ enable_cache: options[:cache_images],
46
+ # PrawnX: no web request unless asked for, since an SVG could otherwise make the server fetch any URL.
47
+ enable_web: options.fetch(:enable_web_requests, false),
48
+ enable_file_with_root: options[:enable_file_requests_with_root]
49
+ )
50
+
51
+ attributes = @root.attributes.dup
52
+ attribute_overrides.each { |key, value| attributes.add(REXML::Attribute.new(key, value)) }
53
+
54
+ @sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, attributes)
55
+ calculate_sizing(requested_width: options[:width], requested_height: options[:height])
56
+
57
+ stylesheets = Prawn::SVG::CSS::Stylesheets.new(css_parser, root, url_loader: url_loader, warnings: warnings)
58
+ @element_styles = stylesheets.load
59
+ process_font_face_rules(stylesheets.font_face_rules)
60
+
61
+ yield self if block_given?
62
+ end
63
+
64
+ def calculate_sizing(requested_width: nil, requested_height: nil)
65
+ sizing.requested_width = requested_width
66
+ sizing.requested_height = requested_height
67
+ sizing.calculate
68
+ end
69
+
70
+ def with_sizing(temporary_sizing)
71
+ original = @sizing
72
+ @sizing = temporary_sizing
73
+ yield
74
+ ensure
75
+ @sizing = original
76
+ end
77
+
78
+ private
79
+
80
+ def process_font_face_rules(rules)
81
+ return unless font_registry
82
+
83
+ rules.each do |declarations|
84
+ decl_hash = {}
85
+ declarations.each { |name, value, _| decl_hash[name] = value }
86
+
87
+ family = unquote_css_string(decl_hash['font-family'])
88
+ next unless family
89
+
90
+ src = decl_hash['src']
91
+ next unless src
92
+
93
+ weight = decl_hash['font-weight']
94
+ style = decl_hash['font-style']
95
+ stretch = decl_hash['font-stretch']
96
+
97
+ load_font_face_source(family, weight, style, stretch, src)
98
+ end
99
+ end
100
+
101
+ def load_font_face_source(family, weight, style, stretch, src)
102
+ sources = Prawn::SVG::CSS::FontFaceParser.parse_src(src)
103
+
104
+ sources.detect do |source|
105
+ case source[:type]
106
+ when :local
107
+ load_local_font_face(family, weight, style, stretch, source[:name])
108
+ when :url
109
+ load_url_font_face(family, weight, style, stretch, source)
110
+ end
111
+ end
112
+ end
113
+
114
+ def load_local_font_face(family, weight, style, stretch, local_name)
115
+ font_data = font_registry.find_local_font_data(local_name, weight, style, stretch)
116
+ return unless font_data
117
+
118
+ font_registry.register_font_face(family, weight, style, stretch, font_data)
119
+ true
120
+ end
121
+
122
+ def load_url_font_face(family, weight, style, stretch, source)
123
+ return if source[:format] && !Prawn::SVG::CSS::FontFaceParser::SUPPORTED_FORMATS.include?(source[:format])
124
+
125
+ data = url_loader.load(source[:url])
126
+ tempfile = Tempfile.new(['prawn-svg-font', '.ttf'])
127
+ tempfile.binmode
128
+ tempfile.write(data)
129
+ tempfile.close
130
+ @font_face_tempfiles << tempfile
131
+
132
+ font_registry.register_font_face(family, weight, style, stretch, tempfile.path)
133
+ true
134
+ rescue Prawn::SVG::UrlLoader::Error => e
135
+ warnings << "Failed to load @font-face font from #{source[:url]}: #{e.message}"
136
+ false
137
+ end
138
+
139
+ def unquote_css_string(value)
140
+ return unless value
141
+
142
+ value = value.strip
143
+ if (value.start_with?('"') && value.end_with?('"')) || (value.start_with?("'") && value.end_with?("'"))
144
+ value[1..-2]
145
+ else
146
+ value
147
+ end
148
+ end
149
+
150
+ def load_color_mode
151
+ case @options[:color_mode]
152
+ when nil, :rgb then :rgb
153
+ when :cmyk then :cmyk
154
+ else
155
+ raise ArgumentError, ':color_mode must be set to :rgb (default) or :cmyk'
156
+ end
157
+ end
158
+
159
+ def raise_parse_error(data, exception_message = nil)
160
+ message = 'The data supplied is not a valid SVG document.'
161
+
162
+ if data.respond_to?(:end_with?) && data.end_with?('.svg')
163
+ message += " It looks like you've supplied a filename instead; use File.read(filename) to get the data from the file before you pass it to prawn-svg."
164
+ end
165
+
166
+ message += "\n#{exception_message}" if exception_message
167
+
168
+ raise InvalidSVGData, message
169
+ end
170
+ end
@@ -0,0 +1,10 @@
1
+ class Prawn::SVG::Elements::Anchor < Prawn::SVG::Elements::Base
2
+ def parse
3
+ href = href_attribute
4
+ state.anchor_href = href unless href.nil? || href.strip.empty?
5
+ end
6
+
7
+ def container?
8
+ true
9
+ end
10
+ end