prawn-svg 0.32.0 → 0.34.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +20 -3
  3. data/.gitignore +1 -0
  4. data/README.md +32 -24
  5. data/lib/prawn/svg/attributes/stroke.rb +5 -0
  6. data/lib/prawn/svg/color.rb +46 -22
  7. data/lib/prawn/svg/css/selector_parser.rb +11 -11
  8. data/lib/prawn/svg/css/stylesheets.rb +4 -4
  9. data/lib/prawn/svg/document.rb +20 -4
  10. data/lib/prawn/svg/elements/base.rb +10 -11
  11. data/lib/prawn/svg/elements/gradient.rb +2 -2
  12. data/lib/prawn/svg/elements/image.rb +54 -21
  13. data/lib/prawn/svg/elements/path.rb +0 -2
  14. data/lib/prawn/svg/elements/root.rb +1 -1
  15. data/lib/prawn/svg/elements/text_component.rb +12 -3
  16. data/lib/prawn/svg/elements/use.rb +20 -12
  17. data/lib/prawn/svg/elements.rb +1 -0
  18. data/lib/prawn/svg/gradients.rb +48 -0
  19. data/lib/prawn/svg/interface.rb +17 -187
  20. data/lib/prawn/svg/loaders/data.rb +4 -3
  21. data/lib/prawn/svg/properties.rb +2 -0
  22. data/lib/prawn/svg/renderer.rb +236 -0
  23. data/lib/prawn/svg/version.rb +1 -1
  24. data/lib/prawn-svg.rb +2 -0
  25. data/prawn-svg.gemspec +18 -16
  26. data/spec/prawn/svg/color_spec.rb +29 -16
  27. data/spec/prawn/svg/document_spec.rb +17 -0
  28. data/spec/prawn/svg/elements/marker_spec.rb +1 -1
  29. data/spec/prawn/svg/elements/root_spec.rb +28 -0
  30. data/spec/prawn/svg/elements/text_spec.rb +27 -0
  31. data/spec/prawn/svg/interface_spec.rb +10 -2
  32. data/spec/sample_images/image_svg_embed.svg +6 -0
  33. data/spec/sample_svg/cmyk.svg +5 -0
  34. data/spec/sample_svg/gradients-cmyk.svg +40 -0
  35. data/spec/sample_svg/image_svg.svg +9 -0
  36. data/spec/sample_svg/ordering.svg +12 -0
  37. data/spec/sample_svg/symbol.svg +12 -0
  38. data/spec/spec_helper.rb +29 -5
  39. metadata +67 -23
@@ -9,6 +9,10 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
9
9
  raise SkipElementError, "<text> elements are not supported in clip paths"
10
10
  end
11
11
 
12
+ if state.text.nil?
13
+ raise SkipElementError, "attempted to <use> an component inside a text element, this is not supported"
14
+ end
15
+
12
16
  state.text.x = (attributes['x'] || "").split(COMMA_WSP_REGEXP).collect { |n| x(n) }
13
17
  state.text.y = (attributes['y'] || "").split(COMMA_WSP_REGEXP).collect { |n| y(n) }
14
18
  state.text.dx = (attributes['dx'] || "").split(COMMA_WSP_REGEXP).collect { |n| x_pixels(n) }
@@ -41,14 +45,15 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
41
45
  font = select_font
42
46
  apply_font(font) if font
43
47
 
44
- # text_anchor isn't a Prawn option; we have to do some math to support it
45
- # and so we handle this in Prawn::SVG::Interface#rewrite_call_arguments
48
+ # text_anchor and dominant_baseline aren't Prawn options; we have to do some math to support them
49
+ # and so we handle them in Prawn::SVG::Interface#rewrite_call_arguments
46
50
  opts = {
47
51
  size: computed_properties.numerical_font_size,
48
52
  style: font && font.subfamily,
49
53
  text_anchor: computed_properties.text_anchor,
50
54
  }
51
55
 
56
+ opts[:dominant_baseline] = computed_properties.dominant_baseline unless computed_properties.dominant_baseline == 'auto'
52
57
  opts[:decoration] = computed_properties.text_decoration unless computed_properties.text_decoration == 'none'
53
58
 
54
59
  if state.text.parent
@@ -171,7 +176,11 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
171
176
 
172
177
  def svg_text_children
173
178
  text_children.select do |child|
174
- child.node_type == :text || child.namespace == SVG_NAMESPACE || child.namespace == ''
179
+ child.node_type == :text || (
180
+ child.node_type == :element && (
181
+ child.namespace == SVG_NAMESPACE || child.namespace == ''
182
+ )
183
+ )
175
184
  end
176
185
  end
177
186
 
@@ -1,15 +1,12 @@
1
1
  class Prawn::SVG::Elements::Use < Prawn::SVG::Elements::Base
2
- attr_reader :referenced_element_class
3
- attr_reader :referenced_element_source
2
+ attr_reader :referenced_element_class, :referenced_element_source
4
3
 
5
4
  def parse
6
5
  href = href_attribute
7
- if href.nil?
8
- raise SkipElementError, "use tag must have an href or xlink:href"
9
- end
6
+ raise SkipElementError, 'use tag must have an href or xlink:href' if href.nil?
10
7
 
11
8
  if href[0..0] != '#'
12
- raise SkipElementError, "use tag has an href that is not a reference to an id; this is not supported"
9
+ raise SkipElementError, 'use tag has an href that is not a reference to an id; this is not supported'
13
10
  end
14
11
 
15
12
  id = href[1..-1]
@@ -29,14 +26,18 @@ class Prawn::SVG::Elements::Use < Prawn::SVG::Elements::Base
29
26
  end
30
27
  end
31
28
 
32
- if referenced_element_class.nil?
33
- raise SkipElementError, "no tag with ID '#{id}' was found, referenced by use tag"
29
+ raise SkipElementError, "no tag with ID '#{id}' was found, referenced by use tag" if referenced_element_class.nil?
30
+
31
+ if referenced_element_source.name == 'symbol'
32
+ @referenced_element_class = Prawn::SVG::Elements::Viewport
34
33
  end
35
34
 
36
35
  state.inside_use = true
37
36
 
38
37
  @x = attributes['x']
39
38
  @y = attributes['y']
39
+ @width = attributes['width']
40
+ @height = attributes['height']
40
41
  end
41
42
 
42
43
  def container?
@@ -45,16 +46,23 @@ class Prawn::SVG::Elements::Use < Prawn::SVG::Elements::Base
45
46
 
46
47
  def apply
47
48
  if @x || @y
48
- add_call_and_enter "translate", x_pixels(@x || 0), -y_pixels(@y || 0)
49
+ add_call_and_enter 'translate', x_pixels(@x || 0), -y_pixels(@y || 0)
49
50
  end
50
51
  end
51
52
 
52
53
  def process_child_elements
53
- add_call "save"
54
+ add_call 'save'
55
+
56
+ source = referenced_element_source.dup
57
+
58
+ if referenced_element_class == Prawn::SVG::Elements::Viewport
59
+ source.attributes['width'] = @width || '100%'
60
+ source.attributes['height'] = @height || '100%'
61
+ end
54
62
 
55
- child = referenced_element_class.new(document, referenced_element_source, calls, state.dup)
63
+ child = referenced_element_class.new(document, source, calls, state.dup)
56
64
  child.process
57
65
 
58
- add_call "restore"
66
+ add_call 'restore'
59
67
  end
60
68
  end
@@ -37,5 +37,6 @@ module Prawn::SVG::Elements
37
37
  foreignObject: Prawn::SVG::Elements::Ignored,
38
38
  :"font-face" => Prawn::SVG::Elements::Ignored,
39
39
  filter: Prawn::SVG::Elements::Ignored, # unsupported
40
+ mask: Prawn::SVG::Elements::Ignored, # unsupported
40
41
  }
41
42
  end
@@ -0,0 +1,48 @@
1
+ module Prawn::SVG
2
+ class Gradients
3
+ def initialize(document)
4
+ @document = document
5
+ @gradients_by_id = {}
6
+ end
7
+
8
+ def [](id)
9
+ id &&= id.strip
10
+ return unless id && id != ''
11
+
12
+ if element = @gradients_by_id[id]
13
+ element
14
+ elsif raw_element = find_raw_gradient_element_by_id(id)
15
+ create_gradient_element(raw_element)
16
+ end
17
+ end
18
+
19
+ def []=(id, gradient)
20
+ @gradients_by_id[id] = gradient
21
+ end
22
+
23
+ private
24
+
25
+ def find_raw_gradient_element_by_id(id)
26
+ raw_element = find_raw_element_by_id(id)
27
+ raw_element if gradient_element?(raw_element)
28
+ end
29
+
30
+ def create_gradient_element(raw_element)
31
+ Elements::Gradient.new(@document, raw_element, [], new_state).tap(&:process)
32
+ end
33
+
34
+ def find_raw_element_by_id(id)
35
+ REXML::XPath.match(@document.root, %(//*[@id="#{id.gsub('"', '\"')}"])).first
36
+ end
37
+
38
+ def gradient_element?(raw_element)
39
+ Elements::TAG_CLASS_MAPPING[raw_element.name.to_sym] == Elements::Gradient
40
+ end
41
+
42
+ def new_state
43
+ State.new.tap do |state|
44
+ state.viewport_sizing = @document.sizing
45
+ end
46
+ end
47
+ end
48
+ end
@@ -5,7 +5,15 @@
5
5
  module Prawn
6
6
  module SVG
7
7
  class Interface
8
- VALID_OPTIONS = [:at, :position, :vposition, :width, :height, :cache_images, :enable_web_requests, :enable_file_requests_with_root, :fallback_font_name]
8
+ VALID_OPTIONS = %i[
9
+ at position vposition width height cache_images enable_web_requests
10
+ enable_file_requests_with_root fallback_font_name color_mode
11
+ ]
12
+
13
+ INHERITABLE_OPTIONS = %i[
14
+ enable_web_requests enable_file_requests_with_root
15
+ cache_images fallback_font_name color_mode
16
+ ]
9
17
 
10
18
  attr_reader :data, :prawn, :document, :options
11
19
 
@@ -25,33 +33,19 @@ module Prawn
25
33
 
26
34
  font_registry = Prawn::SVG::FontRegistry.new(prawn.font_families)
27
35
 
28
- @document = Document.new(data, [prawn.bounds.width, prawn.bounds.height], options, font_registry: font_registry, &block)
36
+ @document = Document.new(
37
+ data, [prawn.bounds.width, prawn.bounds.height], options,
38
+ font_registry: font_registry, &block
39
+ )
40
+
41
+ @renderer = Renderer.new(prawn, document, options)
29
42
  end
30
43
 
31
44
  #
32
45
  # Draws the SVG to the Prawn::Document object.
33
46
  #
34
47
  def draw
35
- if @document.sizing.invalid?
36
- @document.warnings << "Zero or negative sizing data means this SVG cannot be rendered"
37
- return
38
- end
39
-
40
- @document.warnings.clear
41
-
42
- prawn.save_font do
43
- prawn.bounding_box(position, :width => @document.sizing.output_width, :height => @document.sizing.output_height) do
44
- prawn.save_graphics_state do
45
- clip_rectangle 0, 0, @document.sizing.output_width, @document.sizing.output_height
46
-
47
- calls = []
48
- root_element = Prawn::SVG::Elements::Root.new(@document, @document.root, calls)
49
- root_element.process
50
-
51
- proc_creator(prawn, calls).call
52
- end
53
- end
54
- end
48
+ @renderer.draw
55
49
  end
56
50
 
57
51
  def sizing
@@ -63,176 +57,12 @@ module Prawn
63
57
  end
64
58
 
65
59
  def position
66
- @options[:at] || [x_based_on_requested_alignment, y_based_on_requested_alignment]
60
+ @renderer.position
67
61
  end
68
62
 
69
63
  def self.font_path # backwards support for when the font_path used to be stored on this class
70
64
  Prawn::SVG::FontRegistry.font_path
71
65
  end
72
-
73
- private
74
-
75
- def x_based_on_requested_alignment
76
- case options[:position]
77
- when :left, nil
78
- 0
79
- when :center, :centre
80
- (@document.sizing.bounds[0] - @document.sizing.output_width) / 2.0
81
- when :right
82
- @document.sizing.bounds[0] - @document.sizing.output_width
83
- when Numeric
84
- options[:position]
85
- else
86
- raise ArgumentError, "options[:position] must be one of nil, :left, :right, :center or a number"
87
- end
88
- end
89
-
90
- def y_based_on_requested_alignment
91
- case options[:vposition]
92
- when nil
93
- prawn.cursor
94
- when :top
95
- @document.sizing.bounds[1]
96
- when :center, :centre
97
- @document.sizing.bounds[1] - (@document.sizing.bounds[1] - @document.sizing.output_height) / 2.0
98
- when :bottom
99
- @document.sizing.output_height
100
- when Numeric
101
- @document.sizing.bounds[1] - options[:vposition]
102
- else
103
- raise ArgumentError, "options[:vposition] must be one of nil, :top, :right, :bottom or a number"
104
- end
105
- end
106
-
107
- def proc_creator(prawn, calls)
108
- Proc.new {issue_prawn_command(prawn, calls)}
109
- end
110
-
111
- def issue_prawn_command(prawn, calls)
112
- calls.each do |call, arguments, kwarguments, children|
113
- skip = false
114
-
115
- rewrite_call_arguments(prawn, call, arguments, kwarguments) do
116
- issue_prawn_command(prawn, children) if children.any?
117
- skip = true
118
- end
119
-
120
- if skip
121
- # the call has been overridden
122
- elsif children.empty? && call != 'transparent' # some prawn calls complain if they aren't supplied a block
123
- if RUBY_VERSION >= '2.7' || !kwarguments.empty?
124
- prawn.send(call, *arguments, **kwarguments)
125
- else
126
- prawn.send(call, *arguments)
127
- end
128
- else
129
- if RUBY_VERSION >= '2.7' || !kwarguments.empty?
130
- prawn.send(call, *arguments, **kwarguments, &proc_creator(prawn, children))
131
- else
132
- prawn.send(call, *arguments, &proc_creator(prawn, children))
133
- end
134
- end
135
- end
136
- end
137
-
138
- def rewrite_call_arguments(prawn, call, arguments, kwarguments)
139
- case call
140
- when 'text_group'
141
- @cursor = [0, document.sizing.output_height]
142
- yield
143
-
144
- when 'draw_text'
145
- text, options = arguments.first, kwarguments
146
-
147
- at = options.fetch(:at)
148
-
149
- at[0] = @cursor[0] if at[0] == :relative
150
- at[1] = @cursor[1] if at[1] == :relative
151
-
152
- if offset = options.delete(:offset)
153
- at[0] += offset[0]
154
- at[1] -= offset[1]
155
- end
156
-
157
- width = prawn.width_of(text, options.merge(kerning: true))
158
-
159
- if stretch_to_width = options.delete(:stretch_to_width)
160
- factor = stretch_to_width.to_f * 100 / width.to_f
161
- prawn.add_content "#{factor} Tz"
162
- width = stretch_to_width.to_f
163
- end
164
-
165
- if pad_to_width = options.delete(:pad_to_width)
166
- padding_required = pad_to_width.to_f - width.to_f
167
- padding_per_character = padding_required / text.length.to_f
168
- prawn.add_content "#{padding_per_character} Tc"
169
- width = pad_to_width.to_f
170
- end
171
-
172
- case options.delete(:text_anchor)
173
- when 'middle'
174
- at[0] -= width / 2
175
- @cursor = [at[0] + width / 2, at[1]]
176
- when 'end'
177
- at[0] -= width
178
- @cursor = at.dup
179
- else
180
- @cursor = [at[0] + width, at[1]]
181
- end
182
-
183
- decoration = options.delete(:decoration)
184
- if decoration == 'underline'
185
- prawn.save_graphics_state do
186
- prawn.line_width 1
187
- prawn.line [at[0], at[1] - 1.25], [at[0] + width, at[1] - 1.25]
188
- prawn.stroke
189
- end
190
- end
191
-
192
- when 'transformation_matrix'
193
- left = prawn.bounds.absolute_left
194
- top = prawn.bounds.absolute_top
195
- arguments[4] += left - (left * arguments[0] + top * arguments[2])
196
- arguments[5] += top - (left * arguments[1] + top * arguments[3])
197
-
198
- when 'clip'
199
- prawn.add_content "W n" # clip to path
200
- yield
201
-
202
- when 'save'
203
- prawn.save_graphics_state
204
- yield
205
-
206
- when 'restore'
207
- prawn.restore_graphics_state
208
- yield
209
-
210
- when "end_path"
211
- yield
212
- prawn.add_content "n" # end path
213
-
214
- when 'fill_and_stroke'
215
- yield
216
- # prawn (as at 2.0.1 anyway) uses 'b' for its fill_and_stroke. 'b' is 'h' (closepath) + 'B', and we
217
- # never want closepath to be automatically run as it stuffs up many drawing operations, such as dashes
218
- # and line caps, and makes paths close that we didn't ask to be closed when fill is specified.
219
- even_odd = kwarguments[:fill_rule] == :even_odd
220
- content = even_odd ? 'B*' : 'B'
221
- prawn.add_content content
222
-
223
- when 'noop'
224
- yield
225
- end
226
- end
227
-
228
- def clip_rectangle(x, y, width, height)
229
- prawn.move_to x, y
230
- prawn.line_to x + width, y
231
- prawn.line_to x + width, y + height
232
- prawn.line_to x, y + height
233
- prawn.close_path
234
- prawn.add_content "W n" # clip to path
235
- end
236
66
  end
237
67
  end
238
68
  end
@@ -2,14 +2,15 @@ require 'base64'
2
2
 
3
3
  module Prawn::SVG::Loaders
4
4
  class Data
5
- REGEXP = %r[\Adata:image/(png|jpeg);base64(;[a-z0-9]+)*,]i
5
+ REGEXP = %r{\Adata:image/(png|jpeg|svg\+xml);base64(;[a-z0-9]+)*,}i
6
6
 
7
7
  def from_url(url)
8
- return if url[0..4].downcase != "data:"
8
+ return if url[0..4].downcase != 'data:'
9
9
 
10
10
  matches = url.match(REGEXP)
11
11
  if matches.nil?
12
- raise Prawn::SVG::UrlLoader::Error, "prawn-svg only supports base64-encoded image/png and image/jpeg data URLs"
12
+ raise Prawn::SVG::UrlLoader::Error,
13
+ 'prawn-svg only supports base64-encoded image/png, image/jpeg, and image/svg+xml data URLs'
13
14
  end
14
15
 
15
16
  Base64.decode64(matches.post_match)
@@ -34,10 +34,12 @@ class Prawn::SVG::Properties
34
34
  "stroke" => Config.new("none", true, %w(inherit none currentColor)),
35
35
  "stroke-dasharray" => Config.new("none", true, %w(inherit none)),
36
36
  "stroke-linecap" => Config.new("butt", true, %w(inherit butt round square), true),
37
+ "stroke-linejoin" => Config.new("miter", true, %w(inherit miter round bevel), true),
37
38
  "stroke-opacity" => Config.new("1", true),
38
39
  "stroke-width" => Config.new("1", true),
39
40
  "text-anchor" => Config.new("start", true, %w(inherit start middle end), true),
40
41
  'text-decoration' => Config.new('none', true, %w(inherit none underline), true),
42
+ "dominant-baseline" => Config.new("auto", true, %w(inherit auto middle), true),
41
43
  }.freeze
42
44
 
43
45
  PROPERTIES.each do |name, value|
@@ -0,0 +1,236 @@
1
+ module Prawn
2
+ module SVG
3
+ class Renderer
4
+ attr_reader :prawn, :document, :options
5
+
6
+ #
7
+ # Creates a Prawn::SVG object.
8
+ #
9
+ # +data+ is the SVG data to convert. +prawn+ is your Prawn::Document object.
10
+ #
11
+ # See README.md for the options that can be passed to this method.
12
+ #
13
+ def initialize(prawn, document, options)
14
+ @prawn = prawn
15
+ @document = document
16
+ @options = options
17
+ end
18
+
19
+ #
20
+ # Draws the SVG to the Prawn::Document object.
21
+ #
22
+ def draw
23
+ if sizing.invalid?
24
+ document.warnings << 'Zero or negative sizing data means this SVG cannot be rendered'
25
+ return
26
+ end
27
+
28
+ document.warnings.clear
29
+
30
+ prawn.save_font do
31
+ prawn.bounding_box(position, width: sizing.output_width, height: sizing.output_height) do
32
+ prawn.save_graphics_state do
33
+ clip_rectangle 0, 0, sizing.output_width, sizing.output_height
34
+
35
+ calls = []
36
+ root_element = Prawn::SVG::Elements::Root.new(document, document.root, calls)
37
+ root_element.process
38
+
39
+ proc_creator(prawn, calls).call
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def sizing
46
+ document.sizing
47
+ end
48
+
49
+ def position
50
+ options[:at] || [x_based_on_requested_alignment, y_based_on_requested_alignment]
51
+ end
52
+
53
+ private
54
+
55
+ def x_based_on_requested_alignment
56
+ case options[:position]
57
+ when :left, nil
58
+ 0
59
+ when :center, :centre
60
+ (sizing.bounds[0] - sizing.output_width) / 2.0
61
+ when :right
62
+ sizing.bounds[0] - sizing.output_width
63
+ when Numeric
64
+ options[:position]
65
+ else
66
+ raise ArgumentError, 'options[:position] must be one of nil, :left, :right, :center or a number'
67
+ end
68
+ end
69
+
70
+ def y_based_on_requested_alignment
71
+ case options[:vposition]
72
+ when nil
73
+ prawn.cursor
74
+ when :top
75
+ sizing.bounds[1]
76
+ when :center, :centre
77
+ sizing.bounds[1] - (sizing.bounds[1] - sizing.output_height) / 2.0
78
+ when :bottom
79
+ sizing.output_height
80
+ when Numeric
81
+ sizing.bounds[1] - options[:vposition]
82
+ else
83
+ raise ArgumentError, 'options[:vposition] must be one of nil, :top, :right, :bottom or a number'
84
+ end
85
+ end
86
+
87
+ def proc_creator(prawn, calls)
88
+ proc { issue_prawn_command(prawn, calls) }
89
+ end
90
+
91
+ def issue_prawn_command(prawn, calls)
92
+ calls.each do |call, arguments, kwarguments, children|
93
+ skip = false
94
+
95
+ rewrite_call_arguments(prawn, call, arguments, kwarguments) do
96
+ issue_prawn_command(prawn, children) if children.any?
97
+ skip = true
98
+ end
99
+
100
+ if skip
101
+ # the call has been overridden
102
+ elsif children.empty? && call != 'transparent' # some prawn calls complain if they aren't supplied a block
103
+ if RUBY_VERSION >= '2.7' || !kwarguments.empty?
104
+ prawn.send(call, *arguments, **kwarguments)
105
+ else
106
+ prawn.send(call, *arguments)
107
+ end
108
+ elsif RUBY_VERSION >= '2.7' || !kwarguments.empty?
109
+ prawn.send(call, *arguments, **kwarguments, &proc_creator(prawn, children))
110
+ else
111
+ prawn.send(call, *arguments, &proc_creator(prawn, children))
112
+ end
113
+ end
114
+ end
115
+
116
+ def rewrite_call_arguments(prawn, call, arguments, kwarguments)
117
+ case call
118
+ when 'text_group'
119
+ @cursor = [0, sizing.output_height]
120
+ yield
121
+
122
+ when 'draw_text'
123
+ text = arguments.first
124
+ options = kwarguments
125
+
126
+ at = options.fetch(:at)
127
+
128
+ at[0] = @cursor[0] if at[0] == :relative
129
+ at[1] = @cursor[1] if at[1] == :relative
130
+
131
+ case options.delete(:dominant_baseline)
132
+ when 'middle'
133
+ height = prawn.font.height
134
+ at[1] -= height / 2.0
135
+ @cursor = [at[0], at[1]]
136
+ end
137
+
138
+ if offset = options.delete(:offset)
139
+ at[0] += offset[0]
140
+ at[1] -= offset[1]
141
+ end
142
+
143
+ width = prawn.width_of(text, options.merge(kerning: true))
144
+
145
+ if stretch_to_width = options.delete(:stretch_to_width)
146
+ factor = stretch_to_width.to_f * 100 / width.to_f
147
+ prawn.add_content "#{factor} Tz"
148
+ width = stretch_to_width.to_f
149
+ end
150
+
151
+ if pad_to_width = options.delete(:pad_to_width)
152
+ padding_required = pad_to_width.to_f - width.to_f
153
+ padding_per_character = padding_required / text.length.to_f
154
+ prawn.add_content "#{padding_per_character} Tc"
155
+ width = pad_to_width.to_f
156
+ end
157
+
158
+ case options.delete(:text_anchor)
159
+ when 'middle'
160
+ at[0] -= width / 2
161
+ @cursor = [at[0] + width / 2, at[1]]
162
+ when 'end'
163
+ at[0] -= width
164
+ @cursor = at.dup
165
+ else
166
+ @cursor = [at[0] + width, at[1]]
167
+ end
168
+
169
+ decoration = options.delete(:decoration)
170
+ if decoration == 'underline'
171
+ prawn.save_graphics_state do
172
+ prawn.line_width 1
173
+ prawn.line [at[0], at[1] - 1.25], [at[0] + width, at[1] - 1.25]
174
+ prawn.stroke
175
+ end
176
+ end
177
+
178
+ when 'transformation_matrix'
179
+ left = prawn.bounds.absolute_left
180
+ top = prawn.bounds.absolute_top
181
+ arguments[4] += left - (left * arguments[0] + top * arguments[2])
182
+ arguments[5] += top - (left * arguments[1] + top * arguments[3])
183
+
184
+ when 'clip'
185
+ prawn.add_content 'W n' # clip to path
186
+ yield
187
+
188
+ when 'save'
189
+ prawn.save_graphics_state
190
+ yield
191
+
192
+ when 'restore'
193
+ prawn.restore_graphics_state
194
+ yield
195
+
196
+ when 'end_path'
197
+ yield
198
+ prawn.add_content 'n' # end path
199
+
200
+ when 'fill_and_stroke'
201
+ yield
202
+ # prawn (as at 2.0.1 anyway) uses 'b' for its fill_and_stroke. 'b' is 'h' (closepath) + 'B', and we
203
+ # never want closepath to be automatically run as it stuffs up many drawing operations, such as dashes
204
+ # and line caps, and makes paths close that we didn't ask to be closed when fill is specified.
205
+ even_odd = kwarguments[:fill_rule] == :even_odd
206
+ content = even_odd ? 'B*' : 'B'
207
+ prawn.add_content content
208
+
209
+ when 'noop'
210
+ yield
211
+
212
+ when 'svg:render_sub_document'
213
+ sub_document = arguments.first
214
+ sub_options = inheritable_options.merge({ at: [0, 0] })
215
+
216
+ Renderer.new(prawn, sub_document, sub_options).draw
217
+ document.warnings.concat(sub_document.warnings)
218
+ yield
219
+ end
220
+ end
221
+
222
+ def inheritable_options
223
+ (options || {}).slice(Prawn::SVG::Interface::INHERITABLE_OPTIONS)
224
+ end
225
+
226
+ def clip_rectangle(x, y, width, height)
227
+ prawn.move_to x, y
228
+ prawn.line_to x + width, y
229
+ prawn.line_to x + width, y + height
230
+ prawn.line_to x, y + height
231
+ prawn.close_path
232
+ prawn.add_content 'W n' # clip to path
233
+ end
234
+ end
235
+ end
236
+ end
@@ -1,5 +1,5 @@
1
1
  module Prawn
2
2
  module SVG
3
- VERSION = '0.32.0'
3
+ VERSION = '0.34.0'
4
4
  end
5
5
  end
data/lib/prawn-svg.rb CHANGED
@@ -21,12 +21,14 @@ require 'prawn/svg/properties'
21
21
  require 'prawn/svg/pathable'
22
22
  require 'prawn/svg/elements'
23
23
  require 'prawn/svg/extension'
24
+ require 'prawn/svg/renderer'
24
25
  require 'prawn/svg/interface'
25
26
  require 'prawn/svg/css/font_family_parser'
26
27
  require 'prawn/svg/css/selector_parser'
27
28
  require 'prawn/svg/css/stylesheets'
28
29
  require 'prawn/svg/ttf'
29
30
  require 'prawn/svg/font'
31
+ require 'prawn/svg/gradients'
30
32
  require 'prawn/svg/document'
31
33
  require 'prawn/svg/state'
32
34