gamba 2.0.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.
Files changed (163) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/COPYING +2 -0
  4. data/GPLv2 +339 -0
  5. data/GPLv3 +674 -0
  6. data/LICENSE +65 -0
  7. data/LICENSE-prawn-svg +21 -0
  8. data/README.md +148 -0
  9. data/data/fonts/Courier-Bold.afm +342 -0
  10. data/data/fonts/Courier-BoldOblique.afm +342 -0
  11. data/data/fonts/Courier-Oblique.afm +342 -0
  12. data/data/fonts/Courier.afm +342 -0
  13. data/data/fonts/Helvetica-Bold.afm +2827 -0
  14. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  15. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  16. data/data/fonts/Helvetica.afm +3051 -0
  17. data/data/fonts/MustRead.html +19 -0
  18. data/data/fonts/Symbol.afm +213 -0
  19. data/data/fonts/Times-Bold.afm +2588 -0
  20. data/data/fonts/Times-BoldItalic.afm +2384 -0
  21. data/data/fonts/Times-Italic.afm +2667 -0
  22. data/data/fonts/Times-Roman.afm +2419 -0
  23. data/data/fonts/ZapfDingbats.afm +225 -0
  24. data/lib/gamba/annotations.rb +21 -0
  25. data/lib/gamba/combine.rb +307 -0
  26. data/lib/gamba/document/bounding_box.rb +193 -0
  27. data/lib/gamba/document/column_box.rb +91 -0
  28. data/lib/gamba/document/internals.rb +264 -0
  29. data/lib/gamba/document/span.rb +25 -0
  30. data/lib/gamba/document.rb +268 -0
  31. data/lib/gamba/encoding.rb +13 -0
  32. data/lib/gamba/errors.rb +39 -0
  33. data/lib/gamba/font.rb +228 -0
  34. data/lib/gamba/fonts/afm.rb +171 -0
  35. data/lib/gamba/fonts/otf.rb +50 -0
  36. data/lib/gamba/fonts/ttf.rb +413 -0
  37. data/lib/gamba/graphic_state.rb +84 -0
  38. data/lib/gamba/graphics/color.rb +174 -0
  39. data/lib/gamba/graphics/patterns.rb +100 -0
  40. data/lib/gamba/graphics/style.rb +159 -0
  41. data/lib/gamba/graphics.rb +212 -0
  42. data/lib/gamba/grid.rb +216 -0
  43. data/lib/gamba/images/image.rb +271 -0
  44. data/lib/gamba/images/png_decoder.rb +65 -0
  45. data/lib/gamba/images.rb +115 -0
  46. data/lib/gamba/measurement_extensions.rb +40 -0
  47. data/lib/gamba/measurements.rb +62 -0
  48. data/lib/gamba/outline.rb +148 -0
  49. data/lib/gamba/page.rb +125 -0
  50. data/lib/gamba/page_geometry.rb +27 -0
  51. data/lib/gamba/pdf/decryption.rb +165 -0
  52. data/lib/gamba/pdf/outline_node.rb +53 -0
  53. data/lib/gamba/pdf/reader.rb +616 -0
  54. data/lib/gamba/pdf/reference.rb +31 -0
  55. data/lib/gamba/pdf/writer.rb +105 -0
  56. data/lib/gamba/pdf.rb +87 -0
  57. data/lib/gamba/pdf_a/srgb_profile.rb +61 -0
  58. data/lib/gamba/pdf_a.rb +132 -0
  59. data/lib/gamba/repeater.rb +79 -0
  60. data/lib/gamba/security.rb +111 -0
  61. data/lib/gamba/stamp.rb +76 -0
  62. data/lib/gamba/table/cell/kinds.rb +246 -0
  63. data/lib/gamba/table/cell.rb +331 -0
  64. data/lib/gamba/table/cells.rb +208 -0
  65. data/lib/gamba/table.rb +298 -0
  66. data/lib/gamba/text/box.rb +16 -0
  67. data/lib/gamba/text/formatted/arranger.rb +217 -0
  68. data/lib/gamba/text/formatted/box.rb +312 -0
  69. data/lib/gamba/text/formatted/fragment.rb +205 -0
  70. data/lib/gamba/text/formatted/line_wrap.rb +205 -0
  71. data/lib/gamba/text/formatted/parser.rb +150 -0
  72. data/lib/gamba/text/formatted/wrap.rb +103 -0
  73. data/lib/gamba/text.rb +310 -0
  74. data/lib/gamba/transformation_stack.rb +43 -0
  75. data/lib/gamba/true_type/cff_subset.rb +157 -0
  76. data/lib/gamba/true_type/file.rb +372 -0
  77. data/lib/gamba/true_type/open_type_layout.rb +189 -0
  78. data/lib/gamba/true_type/simple_subsets.rb +108 -0
  79. data/lib/gamba/true_type/subset.rb +206 -0
  80. data/lib/gamba/version.rb +10 -0
  81. data/lib/gamba/view.rb +28 -0
  82. data/lib/gamba.rb +124 -0
  83. data/lib/pdf/core.rb +4 -0
  84. data/lib/prawn/measurement_extensions.rb +4 -0
  85. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  86. data/lib/prawn/svg/attributes/mask.rb +16 -0
  87. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  88. data/lib/prawn/svg/attributes/space.rb +10 -0
  89. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  90. data/lib/prawn/svg/attributes/transform.rb +10 -0
  91. data/lib/prawn/svg/attributes.rb +6 -0
  92. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  93. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  94. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  95. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  96. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  97. data/lib/prawn/svg/color.rb +240 -0
  98. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  99. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  100. data/lib/prawn/svg/css/font_parser.rb +46 -0
  101. data/lib/prawn/svg/css/selector_parser.rb +180 -0
  102. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  103. data/lib/prawn/svg/css/values_parser.rb +68 -0
  104. data/lib/prawn/svg/document.rb +169 -0
  105. data/lib/prawn/svg/elements/anchor.rb +10 -0
  106. data/lib/prawn/svg/elements/base.rb +315 -0
  107. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  108. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  109. data/lib/prawn/svg/elements/circle.rb +19 -0
  110. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  111. data/lib/prawn/svg/elements/container.rb +10 -0
  112. data/lib/prawn/svg/elements/direct_render_base.rb +28 -0
  113. data/lib/prawn/svg/elements/ellipse.rb +20 -0
  114. data/lib/prawn/svg/elements/gradient.rb +183 -0
  115. data/lib/prawn/svg/elements/ignored.rb +5 -0
  116. data/lib/prawn/svg/elements/image.rb +178 -0
  117. data/lib/prawn/svg/elements/line.rb +27 -0
  118. data/lib/prawn/svg/elements/marker.rb +72 -0
  119. data/lib/prawn/svg/elements/mask.rb +65 -0
  120. data/lib/prawn/svg/elements/path.rb +294 -0
  121. data/lib/prawn/svg/elements/pattern.rb +150 -0
  122. data/lib/prawn/svg/elements/polygon.rb +25 -0
  123. data/lib/prawn/svg/elements/polyline.rb +23 -0
  124. data/lib/prawn/svg/elements/rect.rb +64 -0
  125. data/lib/prawn/svg/elements/root.rb +22 -0
  126. data/lib/prawn/svg/elements/switch.rb +92 -0
  127. data/lib/prawn/svg/elements/text.rb +99 -0
  128. data/lib/prawn/svg/elements/text_component.rb +352 -0
  129. data/lib/prawn/svg/elements/text_node.rb +308 -0
  130. data/lib/prawn/svg/elements/text_path.rb +153 -0
  131. data/lib/prawn/svg/elements/use.rb +142 -0
  132. data/lib/prawn/svg/elements/viewport.rb +29 -0
  133. data/lib/prawn/svg/elements.rb +46 -0
  134. data/lib/prawn/svg/extension.rb +25 -0
  135. data/lib/prawn/svg/font.rb +20 -0
  136. data/lib/prawn/svg/font_metrics.rb +144 -0
  137. data/lib/prawn/svg/font_registry.rb +315 -0
  138. data/lib/prawn/svg/funciri.rb +14 -0
  139. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  140. data/lib/prawn/svg/gradients.rb +53 -0
  141. data/lib/prawn/svg/interface.rb +72 -0
  142. data/lib/prawn/svg/length.rb +43 -0
  143. data/lib/prawn/svg/link_renderer.rb +40 -0
  144. data/lib/prawn/svg/loaders/data.rb +17 -0
  145. data/lib/prawn/svg/loaders/file.rb +111 -0
  146. data/lib/prawn/svg/loaders/web.rb +24 -0
  147. data/lib/prawn/svg/paint.rb +67 -0
  148. data/lib/prawn/svg/pathable.rb +153 -0
  149. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  150. data/lib/prawn/svg/pdfmatrix.rb +39 -0
  151. data/lib/prawn/svg/percentage.rb +24 -0
  152. data/lib/prawn/svg/properties.rb +329 -0
  153. data/lib/prawn/svg/renderer.rb +249 -0
  154. data/lib/prawn/svg/state.rb +24 -0
  155. data/lib/prawn/svg/transform_parser.rb +78 -0
  156. data/lib/prawn/svg/ttc.rb +35 -0
  157. data/lib/prawn/svg/ttf.rb +95 -0
  158. data/lib/prawn/svg/url_loader.rb +46 -0
  159. data/lib/prawn/svg/version.rb +5 -0
  160. data/lib/prawn/table.rb +4 -0
  161. data/lib/prawn-svg.rb +52 -0
  162. data/lib/prawn.rb +3 -0
  163. metadata +263 -0
@@ -0,0 +1,46 @@
1
+ module Prawn::SVG::CSS
2
+ class FontParser
3
+ def self.parse(string)
4
+ in_quote = nil
5
+ in_escape = false
6
+ in_list_delimiter = false
7
+ current = nil
8
+ values = []
9
+
10
+ string.each_char do |char|
11
+ if in_escape
12
+ in_escape = false
13
+ if current.nil?
14
+ current = char
15
+ values << current
16
+ else
17
+ current << char
18
+ end
19
+ elsif char == "," && in_quote.nil? && !in_escape && current
20
+ current << char
21
+ in_list_delimiter = true
22
+ elsif char == "\\"
23
+ in_escape = true
24
+ elsif current.nil?
25
+ if char.match(/\s/).nil?
26
+ in_list_delimiter = false
27
+ current = char
28
+ values << current
29
+ end
30
+ elsif !in_quote && !in_escape && !in_list_delimiter && char.match(/\s/)
31
+ current = nil
32
+ else
33
+ current << char
34
+ end
35
+
36
+ if char == in_quote
37
+ in_quote = nil
38
+ elsif in_quote.nil? && ['"', "'"].include?(char)
39
+ in_quote = char
40
+ end
41
+ end
42
+
43
+ values.map(&:rstrip)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,180 @@
1
+ module Prawn::SVG::CSS
2
+ class SelectorParser
3
+ ATTRIBUTE_OPERATORS = ["=", "*=", "~=", "^=", "|=", "$=", nil].freeze
4
+
5
+ def self.parse(selector)
6
+ tokens = tokenise_css_selector(selector) or return
7
+
8
+ result = [{}]
9
+ part = nil
10
+
11
+ tokens.each do |token|
12
+ case token
13
+ when Modifier
14
+ part = token.type
15
+ result.last[part] ||= (part == :name) ? +"" : []
16
+ when Identifier
17
+ return nil unless part
18
+
19
+ result.last[part] << token.name
20
+ when Attribute
21
+ return nil unless ATTRIBUTE_OPERATORS.include?(token.operator)
22
+
23
+ (result.last[:attribute] ||= []) << [token.key, token.operator, token.value]
24
+ when Combinator
25
+ result << {combinator: token.type}
26
+ part = nil
27
+ end
28
+ end
29
+
30
+ result
31
+ end
32
+
33
+ VALID_CSS_IDENTIFIER_CHAR = /[a-zA-Z0-9_\u00a0-\uffff-]/
34
+ Identifier = Struct.new(:name)
35
+ Modifier = Struct.new(:type)
36
+ Combinator = Struct.new(:type)
37
+ Attribute = Struct.new(:key, :operator, :value)
38
+
39
+ def self.tokenise_css_selector(selector)
40
+ result = []
41
+ brackets = false
42
+ attribute = false
43
+ quote = false
44
+
45
+ selector.strip.chars do |char|
46
+ if brackets
47
+ result.last.name << char
48
+ brackets = false if char == ")"
49
+ elsif attribute
50
+ case attribute
51
+ when :pre_key
52
+ if VALID_CSS_IDENTIFIER_CHAR.match(char)
53
+ result.last.key = String.new(char)
54
+ attribute = :key
55
+ elsif char != " " && char != "\t"
56
+ return nil
57
+ end
58
+
59
+ when :key
60
+ if VALID_CSS_IDENTIFIER_CHAR.match(char)
61
+ result.last.key << char
62
+ elsif char == "]"
63
+ attribute = nil
64
+ elsif "=*~^|$".include?(char)
65
+ result.last.operator = String.new(char)
66
+ attribute = :operator
67
+ elsif [" ", "\t"].include?(char)
68
+ attribute = :pre_operator
69
+ else
70
+ return nil
71
+ end
72
+
73
+ when :pre_operator
74
+ if "=*~^|$".include?(char)
75
+ result.last.operator = String.new(char)
76
+ attribute = :operator
77
+ elsif char != " " && char != "\t"
78
+ return nil
79
+ end
80
+
81
+ when :operator
82
+ if "=*~^|$".include?(char)
83
+ result.last.operator << char
84
+ elsif [" ", "\t"].include?(char)
85
+ attribute = :pre_value
86
+ elsif ['"', "'"].include?(char)
87
+ result.last.value = +""
88
+ attribute = String.new(char)
89
+ else
90
+ result.last.value = String.new(char)
91
+ attribute = :value
92
+ end
93
+
94
+ when :pre_value
95
+ if ['"', "'"].include?(char)
96
+ result.last.value = +""
97
+ attribute = String.new(char)
98
+ elsif char != " " && char != "\t"
99
+ result.last.value = String.new(char)
100
+ attribute = :value
101
+ end
102
+
103
+ when :value
104
+ if char == "]"
105
+ result.last.value = String.new(result.last.value.rstrip)
106
+ attribute = nil
107
+ else
108
+ result.last.value << char
109
+ end
110
+
111
+ when '"', "'"
112
+ if char == "\\" && !quote
113
+ quote = true
114
+ elsif char == attribute && !quote
115
+ attribute = :post_string
116
+ else
117
+ quote = false
118
+ result.last.value << char
119
+ end
120
+
121
+ when :post_string
122
+ if char == "]"
123
+ attribute = nil
124
+ elsif char != " " && char != "\t"
125
+ return nil
126
+ end
127
+ end
128
+
129
+ elsif VALID_CSS_IDENTIFIER_CHAR.match(char)
130
+ case result.last
131
+ when Identifier
132
+ result.last.name << char
133
+ else
134
+ result << Modifier.new(:name) unless result.last.is_a?(Modifier)
135
+ result << Identifier.new(char)
136
+ end
137
+ else
138
+ case char
139
+ when "."
140
+ result << Modifier.new(:class)
141
+ when "#"
142
+ result << Modifier.new(:id)
143
+ when ":"
144
+ result << Modifier.new(:pseudo_class)
145
+ when " ", "\t"
146
+ result << Combinator.new(:descendant) unless result.last.is_a?(Combinator)
147
+ when ">"
148
+ result.pop if result.last == Combinator.new(:descendant)
149
+ result << Combinator.new(:child)
150
+ when "+"
151
+ result.pop if result.last == Combinator.new(:descendant)
152
+ result << Combinator.new(:adjacent)
153
+ when "~"
154
+ result.pop if result.last == Combinator.new(:descendant)
155
+ result << Combinator.new(:siblings)
156
+ when "*"
157
+ return nil unless result.empty? || result.last.is_a?(Combinator)
158
+
159
+ result << Modifier.new(:name)
160
+ result << Identifier.new("*")
161
+ when "(" # e.g. :nth-child(3n+4)
162
+ unless result.last.is_a?(Identifier) && result[-2] && result[-2].is_a?(Modifier) && result[-2].type == :pseudo_class
163
+ return nil
164
+ end
165
+
166
+ result.last.name << "("
167
+ brackets = true
168
+ when "["
169
+ result << Attribute.new
170
+ attribute = :pre_key
171
+ else
172
+ return nil # unsupported Combinator
173
+ end
174
+ end
175
+ end
176
+
177
+ result unless brackets || attribute
178
+ end
179
+ end
180
+ 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,169 @@
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
+ @url_loader = Prawn::SVG::UrlLoader.new(
44
+ enable_cache: options[:cache_images],
45
+ # Gamba: no web request unless asked for, since an SVG could otherwise make the server fetch any URL.
46
+ enable_web: options.fetch(:enable_web_requests, false),
47
+ enable_file_with_root: options[:enable_file_requests_with_root]
48
+ )
49
+
50
+ attributes = @root.attributes.dup
51
+ attribute_overrides.each { |key, value| attributes.add(REXML::Attribute.new(key, value)) }
52
+
53
+ @sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, attributes)
54
+ calculate_sizing(requested_width: options[:width], requested_height: options[:height])
55
+
56
+ stylesheets = Prawn::SVG::CSS::Stylesheets.new(css_parser, root, url_loader:, warnings:)
57
+ @element_styles = stylesheets.load
58
+ process_font_face_rules(stylesheets.font_face_rules)
59
+
60
+ yield self if block_given?
61
+ end
62
+
63
+ def calculate_sizing(requested_width: nil, requested_height: nil)
64
+ sizing.requested_width = requested_width
65
+ sizing.requested_height = requested_height
66
+ sizing.calculate
67
+ end
68
+
69
+ def with_sizing(temporary_sizing)
70
+ original = @sizing
71
+ @sizing = temporary_sizing
72
+ yield
73
+ ensure
74
+ @sizing = original
75
+ end
76
+
77
+ private
78
+
79
+ def process_font_face_rules(rules)
80
+ return unless font_registry
81
+
82
+ rules.each do |declarations|
83
+ decl_hash = {}
84
+ declarations.each { |name, value, _| decl_hash[name] = value }
85
+
86
+ family = unquote_css_string(decl_hash["font-family"])
87
+ next unless family
88
+
89
+ src = decl_hash["src"]
90
+ next unless src
91
+
92
+ weight = decl_hash["font-weight"]
93
+ style = decl_hash["font-style"]
94
+ stretch = decl_hash["font-stretch"]
95
+
96
+ load_font_face_source(family, weight, style, stretch, src)
97
+ end
98
+ end
99
+
100
+ def load_font_face_source(family, weight, style, stretch, src)
101
+ sources = Prawn::SVG::CSS::FontFaceParser.parse_src(src)
102
+
103
+ sources.detect do |source|
104
+ case source[:type]
105
+ when :local
106
+ load_local_font_face(family, weight, style, stretch, source[:name])
107
+ when :url
108
+ load_url_font_face(family, weight, style, stretch, source)
109
+ end
110
+ end
111
+ end
112
+
113
+ def load_local_font_face(family, weight, style, stretch, local_name)
114
+ font_data = font_registry.find_local_font_data(local_name, weight, style, stretch)
115
+ return unless font_data
116
+
117
+ font_registry.register_font_face(family, weight, style, stretch, font_data)
118
+ true
119
+ end
120
+
121
+ def load_url_font_face(family, weight, style, stretch, source)
122
+ return if source[:format] && !Prawn::SVG::CSS::FontFaceParser::SUPPORTED_FORMATS.include?(source[:format])
123
+
124
+ data = url_loader.load(source[:url])
125
+ tempfile = Tempfile.new(["prawn-svg-font", ".ttf"])
126
+ tempfile.binmode
127
+ tempfile.write(data)
128
+ tempfile.close
129
+ @font_face_tempfiles << tempfile
130
+
131
+ font_registry.register_font_face(family, weight, style, stretch, tempfile.path)
132
+ true
133
+ rescue Prawn::SVG::UrlLoader::Error => e
134
+ warnings << "Failed to load @font-face font from #{source[:url]}: #{e.message}"
135
+ false
136
+ end
137
+
138
+ def unquote_css_string(value)
139
+ return unless value
140
+
141
+ value = value.strip
142
+ if (value.start_with?('"') && value.end_with?('"')) || (value.start_with?("'") && value.end_with?("'"))
143
+ value[1..-2]
144
+ else
145
+ value
146
+ end
147
+ end
148
+
149
+ def load_color_mode
150
+ case @options[:color_mode]
151
+ when nil, :rgb then :rgb
152
+ when :cmyk then :cmyk
153
+ else
154
+ raise ArgumentError, ":color_mode must be set to :rgb (default) or :cmyk"
155
+ end
156
+ end
157
+
158
+ def raise_parse_error(data, exception_message = nil)
159
+ message = "The data supplied is not a valid SVG document."
160
+
161
+ if data.respond_to?(:end_with?) && data.end_with?(".svg")
162
+ 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."
163
+ end
164
+
165
+ message += "\n#{exception_message}" if exception_message
166
+
167
+ raise InvalidSVGData, message
168
+ end
169
+ 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