prawn-svg 0.37.0 → 0.40.2

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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Gemfile.lock +8 -8
  4. data/README.md +57 -35
  5. data/lib/prawn/svg/attributes/clip_path.rb +32 -2
  6. data/lib/prawn/svg/attributes/mask.rb +16 -0
  7. data/lib/prawn/svg/attributes/stroke.rb +11 -1
  8. data/lib/prawn/svg/attributes/transform.rb +2 -0
  9. data/lib/prawn/svg/attributes.rb +1 -1
  10. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +15 -74
  11. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  12. data/lib/prawn/svg/calculators/pixels.rb +15 -0
  13. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  14. data/lib/prawn/svg/css/stylesheets.rb +92 -5
  15. data/lib/prawn/svg/document.rb +98 -2
  16. data/lib/prawn/svg/elements/anchor.rb +9 -0
  17. data/lib/prawn/svg/elements/base.rb +28 -1
  18. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  19. data/lib/prawn/svg/elements/clip_path.rb +17 -2
  20. data/lib/prawn/svg/elements/container.rb +0 -7
  21. data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
  22. data/lib/prawn/svg/elements/gradient.rb +0 -19
  23. data/lib/prawn/svg/elements/image.rb +65 -2
  24. data/lib/prawn/svg/elements/marker.rb +1 -2
  25. data/lib/prawn/svg/elements/mask.rb +65 -0
  26. data/lib/prawn/svg/elements/path.rb +9 -16
  27. data/lib/prawn/svg/elements/pattern.rb +150 -0
  28. data/lib/prawn/svg/elements/polygon.rb +2 -2
  29. data/lib/prawn/svg/elements/rect.rb +40 -9
  30. data/lib/prawn/svg/elements/switch.rb +92 -0
  31. data/lib/prawn/svg/elements/text.rb +80 -52
  32. data/lib/prawn/svg/elements/text_component.rb +293 -197
  33. data/lib/prawn/svg/elements/text_node.rb +308 -0
  34. data/lib/prawn/svg/elements/text_path.rb +153 -0
  35. data/lib/prawn/svg/elements/use.rb +97 -19
  36. data/lib/prawn/svg/elements.rb +8 -5
  37. data/lib/prawn/svg/font.rb +14 -59
  38. data/lib/prawn/svg/font_metrics.rb +152 -0
  39. data/lib/prawn/svg/font_registry.rb +294 -54
  40. data/lib/prawn/svg/gradient_renderer.rb +11 -3
  41. data/lib/prawn/svg/gradients.rb +5 -2
  42. data/lib/prawn/svg/interface.rb +4 -1
  43. data/lib/prawn/svg/link_renderer.rb +40 -0
  44. data/lib/prawn/svg/pathable.rb +8 -5
  45. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  46. data/lib/prawn/svg/properties.rb +125 -32
  47. data/lib/prawn/svg/renderer.rb +71 -70
  48. data/lib/prawn/svg/state.rb +1 -1
  49. data/lib/prawn/svg/ttc.rb +35 -0
  50. data/lib/prawn/svg/ttf.rb +77 -47
  51. data/lib/prawn/svg/version.rb +1 -1
  52. data/lib/prawn-svg.rb +6 -0
  53. data/prawn-svg.gemspec +2 -2
  54. data/scripts/compare_samples +25 -0
  55. data/spec/integration_spec.rb +36 -16
  56. data/spec/prawn/svg/attributes/clip_path_spec.rb +246 -0
  57. data/spec/prawn/svg/attributes/stroke_spec.rb +126 -0
  58. data/spec/prawn/svg/attributes/transform_spec.rb +4 -0
  59. data/spec/prawn/svg/calculators/path_length_spec.rb +141 -0
  60. data/spec/prawn/svg/css/font_face_parser_spec.rb +72 -0
  61. data/spec/prawn/svg/css/selector_parser_spec.rb +6 -0
  62. data/spec/prawn/svg/css/stylesheets_spec.rb +370 -1
  63. data/spec/prawn/svg/document_spec.rb +199 -2
  64. data/spec/prawn/svg/elements/base_spec.rb +2 -2
  65. data/spec/prawn/svg/elements/gradient_spec.rb +1 -1
  66. data/spec/prawn/svg/elements/image_spec.rb +169 -0
  67. data/spec/prawn/svg/elements/line_spec.rb +1 -1
  68. data/spec/prawn/svg/elements/marker_spec.rb +2 -1
  69. data/spec/prawn/svg/elements/mask_spec.rb +382 -0
  70. data/spec/prawn/svg/elements/path_spec.rb +2 -2
  71. data/spec/prawn/svg/elements/pattern_spec.rb +232 -0
  72. data/spec/prawn/svg/elements/polygon_spec.rb +1 -1
  73. data/spec/prawn/svg/elements/polyline_spec.rb +1 -1
  74. data/spec/prawn/svg/elements/switch_spec.rb +310 -0
  75. data/spec/prawn/svg/elements/text_path_spec.rb +183 -0
  76. data/spec/prawn/svg/elements/text_spec.rb +716 -134
  77. data/spec/prawn/svg/elements/use_spec.rb +274 -0
  78. data/spec/prawn/svg/font_metrics_spec.rb +14 -0
  79. data/spec/prawn/svg/font_registry_spec.rb +347 -24
  80. data/spec/prawn/svg/font_spec.rb +67 -8
  81. data/spec/prawn/svg/interface_spec.rb +32 -12
  82. data/spec/prawn/svg/pathable_spec.rb +2 -2
  83. data/spec/prawn/svg/properties_spec.rb +198 -0
  84. data/spec/prawn/svg/ttc_spec.rb +31 -0
  85. data/spec/sample_css/import_base.css +4 -0
  86. data/spec/sample_css/import_nested.css +1 -0
  87. data/spec/sample_svg/alignment_baseline.svg +61 -0
  88. data/spec/sample_svg/arc_test_a.svg +186 -0
  89. data/spec/sample_svg/arc_test_b.svg +194 -0
  90. data/spec/sample_svg/baseline_shift.svg +55 -0
  91. data/spec/sample_svg/bytes.svg +121 -0
  92. data/spec/sample_svg/clip_path_text.svg +104 -0
  93. data/spec/sample_svg/clip_path_units.svg +102 -0
  94. data/spec/sample_svg/css_import.svg +38 -0
  95. data/spec/sample_svg/dominant_baseline.svg +41 -0
  96. data/spec/sample_svg/font_face.svg +77 -0
  97. data/spec/sample_svg/font_stretch.svg +54 -0
  98. data/spec/sample_svg/font_weights.svg +140 -0
  99. data/spec/sample_svg/kerning.svg +45 -0
  100. data/spec/sample_svg/lang.svg +117 -0
  101. data/spec/sample_svg/links.svg +42 -2
  102. data/spec/sample_svg/marker_shorthand.svg +48 -0
  103. data/spec/sample_svg/mask3.svg +13 -0
  104. data/spec/sample_svg/mask3b.svg +13 -0
  105. data/spec/sample_svg/mask_basic.svg +11 -0
  106. data/spec/sample_svg/mask_contentUnits_objectBoundingBox.svg +11 -0
  107. data/spec/sample_svg/mask_gradient.svg +13 -0
  108. data/spec/sample_svg/mask_image.svg +10 -0
  109. data/spec/sample_svg/mask_multiple.svg +15 -0
  110. data/spec/sample_svg/mask_nested.svg +15 -0
  111. data/spec/sample_svg/mask_opacity.svg +21 -0
  112. data/spec/sample_svg/mask_text.svg +10 -0
  113. data/spec/sample_svg/mask_text_gradient.svg +31 -0
  114. data/spec/sample_svg/mask_units_objectBoundingBox.svg +11 -0
  115. data/spec/sample_svg/mask_units_userSpaceOnUse.svg +10 -0
  116. data/spec/sample_svg/mask_with_transform.svg +11 -0
  117. data/spec/sample_svg/multilingual.svg +7 -0
  118. data/spec/sample_svg/pattern.svg +80 -0
  119. data/spec/sample_svg/positioning.svg +26 -0
  120. data/spec/sample_svg/rounded_rectangle.svg +101 -0
  121. data/spec/sample_svg/stroke_dashoffset.svg +48 -0
  122. data/spec/sample_svg/stroke_miterlimit.svg +48 -0
  123. data/spec/sample_svg/subfamilies.svg +5 -1
  124. data/spec/sample_svg/switch.svg +106 -0
  125. data/spec/sample_svg/text-decoration.svg +5 -3
  126. data/spec/sample_svg/text_path.svg +25 -0
  127. data/spec/sample_svg/text_use.svg +37 -0
  128. data/spec/sample_svg/use_external.svg +51 -0
  129. data/spec/sample_svg/use_external_icons.svg +29 -0
  130. data/spec/sample_svg/vertical-text.svg +152 -0
  131. data/spec/sample_svg/view.svg +48 -0
  132. data/spec/sample_svg/word_spacing.svg +45 -0
  133. data/spec/sample_ttf/TestFamily.ttc +0 -0
  134. metadata +86 -7
  135. data/lib/prawn/svg/elements/depth_first_base.rb +0 -52
@@ -0,0 +1,72 @@
1
+ module Prawn::SVG::CSS
2
+ class FontFaceParser
3
+ SUPPORTED_FORMATS = %w[truetype opentype].freeze
4
+
5
+ class << self
6
+ def parse_src(src)
7
+ split_sources(src).filter_map { |entry| parse_source_entry(entry.strip) }
8
+ end
9
+
10
+ private
11
+
12
+ def split_sources(value)
13
+ entries = []
14
+ current = +''
15
+ depth = 0
16
+ in_quote = nil
17
+
18
+ value.each_char do |char|
19
+ if in_quote
20
+ current << char
21
+ in_quote = nil if char == in_quote
22
+ elsif ['"', "'"].include?(char)
23
+ in_quote = char
24
+ current << char
25
+ elsif char == '('
26
+ depth += 1
27
+ current << char
28
+ elsif char == ')'
29
+ depth -= 1
30
+ current << char
31
+ elsif char == ',' && depth.zero?
32
+ entries << current
33
+ current = +''
34
+ else
35
+ current << char
36
+ end
37
+ end
38
+
39
+ entries << current unless current.strip.empty?
40
+ entries
41
+ end
42
+
43
+ def parse_source_entry(entry)
44
+ parts = Prawn::SVG::CSS::ValuesParser.parse(entry)
45
+ return if parts.empty?
46
+
47
+ first = parts[0]
48
+ return unless first.is_a?(Array)
49
+
50
+ case first[0]
51
+ when 'url'
52
+ url = first[1][0]
53
+ return unless url
54
+
55
+ format = extract_format(parts[1])
56
+ { type: :url, url: url, format: format }
57
+ when 'local'
58
+ name = first[1][0]
59
+ return unless name
60
+
61
+ { type: :local, name: name }
62
+ end
63
+ end
64
+
65
+ def extract_format(part)
66
+ return unless part.is_a?(Array) && part[0] == 'format'
67
+
68
+ part[1][0]
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,11 +1,16 @@
1
1
  module Prawn::SVG::CSS
2
2
  class Stylesheets
3
- attr_reader :css_parser, :root, :media
3
+ attr_reader :css_parser, :root, :media, :font_face_rules
4
4
 
5
- def initialize(css_parser, root, media = :all)
5
+ def initialize(css_parser, root, media = :all, url_loader: nil, warnings: [])
6
6
  @css_parser = css_parser
7
7
  @root = root
8
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
9
14
  end
10
15
 
11
16
  def load
@@ -16,10 +21,35 @@ module Prawn::SVG::CSS
16
21
 
17
22
  private
18
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
+
19
44
  def load_style_elements
20
45
  REXML::XPath.match(root, '//style').each do |source|
21
46
  data = source.texts.map(&:value).join
22
- css_parser.add_block!(data)
47
+
48
+ if @has_url_loader
49
+ css_parser.add_block!(data, base_uri: '')
50
+ else
51
+ css_parser.add_block!(data)
52
+ end
23
53
  end
24
54
  end
25
55
 
@@ -32,6 +62,11 @@ module Prawn::SVG::CSS
32
62
  rule_set.each_declaration { |*data| declarations << data }
33
63
 
34
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
+
35
70
  next unless (selector = Prawn::SVG::CSS::SelectorParser.parse(selector_text))
36
71
 
37
72
  xpath = css_selector_to_xpath(selector)
@@ -62,10 +97,13 @@ module Prawn::SVG::CSS
62
97
  %("#{value.gsub('\\', '\\\\').gsub('"', '\\"')}") if value
63
98
  end
64
99
 
100
+ RECOGNIZED_PSEUDO_CLASSES = %w[first-child last-child link visited hover active focus].freeze
101
+
65
102
  def css_selector_to_xpath(selector)
66
103
  selector.map do |element|
67
104
  pseudo_classes = Set.new(element[:pseudo_class])
68
105
  require_function_name = false
106
+ never_match = false
69
107
 
70
108
  result = case element[:combinator]
71
109
  when :child
@@ -80,11 +118,18 @@ module Prawn::SVG::CSS
80
118
  end
81
119
 
82
120
  positions = []
121
+ lang_predicates = []
122
+ href_predicates = []
83
123
  pseudo_classes.each do |pc|
84
124
  case pc
85
125
  when 'first-child' then positions << '1'
86
126
  when 'last-child' then positions << 'last()'
87
- when /^nth-child\((\d+)\)$/ then positions << $1
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)
88
133
  end
89
134
  end
90
135
 
@@ -119,7 +164,7 @@ module Prawn::SVG::CSS
119
164
  when '^='
120
165
  result << "[starts-with(@#{key}, #{xpath_quote value})]"
121
166
  when '$='
122
- result << "[substring(@#{key}, string-length(@#{key}) - #{value.length - 1}) = #{xpath_quote value})]"
167
+ result << "[substring(@#{key}, string-length(@#{key}) - #{value.length - 1}) = #{xpath_quote value}]"
123
168
  when '*='
124
169
  result << "[contains(@#{key}, #{xpath_quote value})]"
125
170
  when '~='
@@ -129,10 +174,52 @@ module Prawn::SVG::CSS
129
174
  end
130
175
  end
131
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
+
132
185
  result
133
186
  end.join
134
187
  end
135
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
+
136
223
  def calculate_specificity(selector)
137
224
  selector.reduce([0, 0, 0]) do |(a, b, c), element|
138
225
  [
@@ -1,9 +1,15 @@
1
+ require 'tempfile'
2
+
1
3
  class Prawn::SVG::Document
2
4
  Error = Class.new(StandardError)
3
5
  InvalidSVGData = Class.new(Error)
4
6
 
5
7
  DEFAULT_FALLBACK_FONT_NAME = 'Times-Roman'.freeze
6
8
 
9
+ class << self
10
+ attr_accessor :enable_web_requests_warned
11
+ end
12
+
7
13
  # An +Array+ of warnings that occurred while parsing the SVG data.
8
14
  attr_reader :warnings
9
15
 
@@ -14,7 +20,8 @@ class Prawn::SVG::Document
14
20
  :url_loader,
15
21
  :elements_by_id, :gradients,
16
22
  :element_styles,
17
- :color_mode
23
+ :color_mode,
24
+ :external_svg_cache
18
25
 
19
26
  def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new, attribute_overrides: {})
20
27
  begin
@@ -27,9 +34,18 @@ class Prawn::SVG::Document
27
34
  @options = options
28
35
  @elements_by_id = {}
29
36
  @gradients = Prawn::SVG::Gradients.new(self)
37
+ @external_svg_cache = {}
30
38
  @fallback_font_name = options.fetch(:fallback_font_name, DEFAULT_FALLBACK_FONT_NAME)
31
39
  @font_registry = font_registry
32
40
  @color_mode = load_color_mode
41
+ @font_face_tempfiles = []
42
+
43
+ if !options.key?(:enable_web_requests) && !self.class.enable_web_requests_warned
44
+ self.class.enable_web_requests_warned = true
45
+ warn '[prawn-svg] WARNING: :enable_web_requests is not set and currently defaults to true. ' \
46
+ 'In prawn-svg 1.0, this will default to false. ' \
47
+ 'Please explicitly pass enable_web_requests: true or enable_web_requests: false to suppress this warning.'
48
+ end
33
49
 
34
50
  @url_loader = Prawn::SVG::UrlLoader.new(
35
51
  enable_cache: options[:cache_images],
@@ -43,7 +59,9 @@ class Prawn::SVG::Document
43
59
  @sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, attributes)
44
60
  calculate_sizing(requested_width: options[:width], requested_height: options[:height])
45
61
 
46
- @element_styles = Prawn::SVG::CSS::Stylesheets.new(css_parser, root).load
62
+ stylesheets = Prawn::SVG::CSS::Stylesheets.new(css_parser, root, url_loader: url_loader, warnings: warnings)
63
+ @element_styles = stylesheets.load
64
+ process_font_face_rules(stylesheets.font_face_rules)
47
65
 
48
66
  yield self if block_given?
49
67
  end
@@ -54,8 +72,86 @@ class Prawn::SVG::Document
54
72
  sizing.calculate
55
73
  end
56
74
 
75
+ def with_sizing(temporary_sizing)
76
+ original = @sizing
77
+ @sizing = temporary_sizing
78
+ yield
79
+ ensure
80
+ @sizing = original
81
+ end
82
+
57
83
  private
58
84
 
85
+ def process_font_face_rules(rules)
86
+ return unless font_registry
87
+
88
+ rules.each do |declarations|
89
+ decl_hash = {}
90
+ declarations.each { |name, value, _| decl_hash[name] = value }
91
+
92
+ family = unquote_css_string(decl_hash['font-family'])
93
+ next unless family
94
+
95
+ src = decl_hash['src']
96
+ next unless src
97
+
98
+ weight = decl_hash['font-weight']
99
+ style = decl_hash['font-style']
100
+ stretch = decl_hash['font-stretch']
101
+
102
+ load_font_face_source(family, weight, style, stretch, src)
103
+ end
104
+ end
105
+
106
+ def load_font_face_source(family, weight, style, stretch, src)
107
+ sources = Prawn::SVG::CSS::FontFaceParser.parse_src(src)
108
+
109
+ sources.detect do |source|
110
+ case source[:type]
111
+ when :local
112
+ load_local_font_face(family, weight, style, stretch, source[:name])
113
+ when :url
114
+ load_url_font_face(family, weight, style, stretch, source)
115
+ end
116
+ end
117
+ end
118
+
119
+ def load_local_font_face(family, weight, style, stretch, local_name)
120
+ font_data = font_registry.find_local_font_data(local_name, weight, style, stretch)
121
+ return unless font_data
122
+
123
+ font_registry.register_font_face(family, weight, style, stretch, font_data)
124
+ true
125
+ end
126
+
127
+ def load_url_font_face(family, weight, style, stretch, source)
128
+ return if source[:format] && !Prawn::SVG::CSS::FontFaceParser::SUPPORTED_FORMATS.include?(source[:format])
129
+
130
+ data = url_loader.load(source[:url])
131
+ tempfile = Tempfile.new(['prawn-svg-font', '.ttf'])
132
+ tempfile.binmode
133
+ tempfile.write(data)
134
+ tempfile.close
135
+ @font_face_tempfiles << tempfile
136
+
137
+ font_registry.register_font_face(family, weight, style, stretch, tempfile.path)
138
+ true
139
+ rescue Prawn::SVG::UrlLoader::Error => e
140
+ warnings << "Failed to load @font-face font from #{source[:url]}: #{e.message}"
141
+ false
142
+ end
143
+
144
+ def unquote_css_string(value)
145
+ return unless value
146
+
147
+ value = value.strip
148
+ if (value.start_with?('"') && value.end_with?('"')) || (value.start_with?("'") && value.end_with?("'"))
149
+ value[1..-2]
150
+ else
151
+ value
152
+ end
153
+ end
154
+
59
155
  def load_color_mode
60
156
  case @options[:color_mode]
61
157
  when nil, :rgb then :rgb
@@ -0,0 +1,9 @@
1
+ class Prawn::SVG::Elements::Anchor < Prawn::SVG::Elements::Base
2
+ def parse
3
+ state.anchor_href = href_attribute
4
+ end
5
+
6
+ def container?
7
+ true
8
+ end
9
+ end
@@ -2,12 +2,14 @@ class Prawn::SVG::Elements::Base
2
2
  extend Forwardable
3
3
 
4
4
  include Prawn::SVG::Elements::CallDuplicator
5
+ include Prawn::SVG::Elements::BboxScaling
5
6
 
6
7
  include Prawn::SVG::Calculators::Pixels
7
8
 
8
9
  include Prawn::SVG::Attributes::Transform
9
10
  include Prawn::SVG::Attributes::Opacity
10
11
  include Prawn::SVG::Attributes::ClipPath
12
+ include Prawn::SVG::Attributes::Mask
11
13
  include Prawn::SVG::Attributes::Stroke
12
14
  include Prawn::SVG::Attributes::Space
13
15
 
@@ -36,7 +38,7 @@ class Prawn::SVG::Elements::Base
36
38
  @attributes = {}
37
39
  @properties = Prawn::SVG::Properties.new
38
40
 
39
- if source && !state.inside_use
41
+ if source && add_to_elements_by_id? && !state.inside_use
40
42
  id = source.attributes['id']
41
43
  id = id.strip if id
42
44
 
@@ -56,6 +58,10 @@ class Prawn::SVG::Elements::Base
56
58
  apply_calls_from_standard_attributes
57
59
  apply
58
60
 
61
+ if state.anchor_href && bounding_box
62
+ add_call('svg:add_link', state.anchor_href, bounding_box)
63
+ end
64
+
59
65
  process_child_elements if container?
60
66
 
61
67
  append_calls_to_parent unless computed_properties.display == 'none'
@@ -114,6 +120,10 @@ class Prawn::SVG::Elements::Base
114
120
  @calls.concat duplicate_calls(other.base_calls)
115
121
  end
116
122
 
123
+ def add_yield_call(&block)
124
+ add_call('svg:yield', block)
125
+ end
126
+
117
127
  def new_call_context_from_base
118
128
  old_calls = @calls
119
129
  @calls = @base_calls
@@ -150,6 +160,7 @@ class Prawn::SVG::Elements::Base
150
160
  parse_transform_attribute_and_call
151
161
  parse_opacity_attributes_and_call
152
162
  parse_clip_path_attribute_and_call
163
+ parse_mask_attribute_and_call
153
164
  apply_colors
154
165
  parse_stroke_attributes_and_call
155
166
  apply_drawing_call
@@ -191,6 +202,9 @@ class Prawn::SVG::Elements::Base
191
202
  add_call "#{type}_color", color.value
192
203
  when Prawn::SVG::Elements::Gradient
193
204
  add_call 'svg:render_gradient', type.to_sym, **color.gradient_arguments(self)
205
+ when Prawn::SVG::Elements::Pattern
206
+ args = color.pattern_arguments(self)
207
+ add_call 'svg:render_pattern', type.to_sym, **args if args
194
208
  when nil
195
209
  nil
196
210
  else
@@ -269,6 +283,14 @@ class Prawn::SVG::Elements::Base
269
283
  ['hidden', 'scroll'].include?(computed_properties.overflow)
270
284
  end
271
285
 
286
+ def transformable?
287
+ true
288
+ end
289
+
290
+ def add_to_elements_by_id?
291
+ true
292
+ end
293
+
272
294
  def stroke_width
273
295
  if computed_properties.stroke.none?
274
296
  0
@@ -282,4 +304,9 @@ class Prawn::SVG::Elements::Base
282
304
  document.element_styles[new_source] = document.element_styles[source]
283
305
  new_source
284
306
  end
307
+
308
+ def set_display_none
309
+ properties.display = 'none'
310
+ computed_properties.display = 'none'
311
+ end
285
312
  end
@@ -0,0 +1,78 @@
1
+ module Prawn::SVG::Elements::BboxScaling
2
+ private
3
+
4
+ def build_object_bounding_box_calls(bbox, source_element = source)
5
+ bbox_left = bbox[0]
6
+ bbox_top = bbox[1]
7
+ bbox_right = bbox[2]
8
+ bbox_bottom = bbox[3]
9
+ bbox_w = bbox_right - bbox_left
10
+ bbox_h = bbox_top - bbox_bottom
11
+
12
+ unit_sizing = Prawn::SVG::Calculators::DocumentSizing.new([1, 1])
13
+ unit_sizing.document_width = 1
14
+ unit_sizing.document_height = 1
15
+ unit_sizing.calculate
16
+
17
+ result_calls = document.with_sizing(unit_sizing) do
18
+ new_state = state.dup
19
+ new_state.viewport_sizing = unit_sizing
20
+ new_state.inside_use = true
21
+
22
+ container = Prawn::SVG::Elements::Container.new(document, source_element, [], new_state)
23
+ container.process
24
+
25
+ container.base_calls
26
+ end
27
+
28
+ scale_calls_to_bbox(duplicate_calls(result_calls), bbox_left, bbox_top, bbox_w, bbox_h)
29
+ end
30
+
31
+ def scale_calls_to_bbox(calls, bbox_left, bbox_top, bbox_w, bbox_h)
32
+ calls.map do |name, args, kwargs, children|
33
+ new_args = case name
34
+ when 'rectangle'
35
+ point, width, height = args
36
+ [scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h), width * bbox_w, height * bbox_h]
37
+ when 'rounded_rectangle'
38
+ point, width, height, radius = args
39
+ [scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h), width * bbox_w, height * bbox_h, radius * bbox_w]
40
+ when 'move_to', 'line_to'
41
+ [scale_point(args[0], bbox_left, bbox_top, bbox_w, bbox_h)]
42
+ when 'circle'
43
+ point, radius = args
44
+ scaled_point = scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h)
45
+ if bbox_w == bbox_h
46
+ [scaled_point, radius * bbox_w]
47
+ else
48
+ new_children = children.any? ? scale_calls_to_bbox(children, bbox_left, bbox_top, bbox_w, bbox_h) : children
49
+ next ['ellipse', [scaled_point, radius * bbox_w, radius * bbox_h], kwargs, new_children]
50
+ end
51
+ when 'ellipse'
52
+ point, rx, ry = args
53
+ [scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h), rx * bbox_w, ry * bbox_h]
54
+ when 'curve_to'
55
+ dest = scale_point(args[0], bbox_left, bbox_top, bbox_w, bbox_h)
56
+ [dest]
57
+ else
58
+ args
59
+ end
60
+
61
+ new_kwargs = if name == 'curve_to' && kwargs[:bounds]
62
+ b = kwargs[:bounds]
63
+ { bounds: b.map { |p| scale_point(p, bbox_left, bbox_top, bbox_w, bbox_h) } }
64
+ else
65
+ kwargs
66
+ end
67
+
68
+ new_children = children.any? ? scale_calls_to_bbox(children, bbox_left, bbox_top, bbox_w, bbox_h) : children
69
+ [name, new_args, new_kwargs, new_children]
70
+ end
71
+ end
72
+
73
+ def scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h)
74
+ x = bbox_left + (point[0] * bbox_w)
75
+ y = bbox_top - ((1.0 - point[1]) * bbox_h)
76
+ [x, y]
77
+ end
78
+ end
@@ -1,11 +1,26 @@
1
1
  class Prawn::SVG::Elements::ClipPath < Prawn::SVG::Elements::Base
2
2
  def parse
3
3
  state.inside_clip_path = true
4
- properties.display = 'none'
5
- computed_properties.display = 'none'
4
+ set_display_none
6
5
  end
7
6
 
8
7
  def container?
9
8
  true
10
9
  end
10
+
11
+ def build_clip_calls(element)
12
+ units = attributes['clipPathUnits'] || 'userSpaceOnUse'
13
+
14
+ if units == 'objectBoundingBox'
15
+ bbox = element.bounding_box
16
+ if bbox.nil?
17
+ document.warnings << 'clipPath with clipPathUnits="objectBoundingBox" requires element to have a bounding box'
18
+ return nil
19
+ end
20
+
21
+ build_object_bounding_box_calls(bbox)
22
+ else
23
+ duplicate_calls(base_calls)
24
+ end
25
+ end
11
26
  end
@@ -7,11 +7,4 @@ class Prawn::SVG::Elements::Container < Prawn::SVG::Elements::Base
7
7
  def container?
8
8
  true
9
9
  end
10
-
11
- private
12
-
13
- def set_display_none
14
- properties.display = 'none'
15
- computed_properties.display = 'none'
16
- end
17
10
  end
@@ -0,0 +1,27 @@
1
+ #
2
+ # This element base class is used for elements that render directly to the Prawn canvas.
3
+ #
4
+ # Initially when I wrote prawn-svg, I was expecting to have multiple renderers and thought separating the codebase
5
+ # from the Prawn renderer would be a good idea. However, it turns out that the Prawn renderer was the only one
6
+ # that was targeted, and it ended up being tightly coupled with the Prawn library.
7
+ #
8
+ # This class is probably how I should have written it. Direct render is required to do text rendering properly
9
+ # because we need to know the width of all the things we print. As of the time of this comment it's the only
10
+ # system that uses DirectRenderBase in prawn-svg.
11
+ #
12
+ class Prawn::SVG::Elements::DirectRenderBase < Prawn::SVG::Elements::Base
13
+ # Called by Renderer when it finds the svg:render call added below.
14
+ def render(prawn, renderer); end
15
+
16
+ protected
17
+
18
+ def parse_and_apply
19
+ parse_standard_attributes
20
+ parse
21
+ apply_calls_from_standard_attributes
22
+ @parent_calls << ['svg:render', [self], [], []] unless computed_properties.display == 'none'
23
+ rescue SkipElementQuietly
24
+ rescue SkipElementError => e
25
+ @document.warnings << e.message
26
+ end
27
+ end
@@ -181,23 +181,4 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
181
181
  end
182
182
  end
183
183
  end
184
-
185
- def percentage_or_proportion(string, default = 0)
186
- string = string.to_s.strip
187
- percentage = false
188
-
189
- if string[-1] == '%'
190
- percentage = true
191
- string = string[0..-2]
192
- end
193
-
194
- value = Float(string, exception: false)
195
- return default unless value
196
-
197
- if percentage
198
- value / 100.0
199
- else
200
- value
201
- end
202
- end
203
184
  end