prawn-svg 0.24.0 → 0.25.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -9
  3. data/lib/prawn/svg/attributes.rb +1 -1
  4. data/lib/prawn/svg/attributes/space.rb +10 -0
  5. data/lib/prawn/svg/attributes/stroke.rb +2 -2
  6. data/lib/prawn/svg/attributes/transform.rb +2 -2
  7. data/lib/prawn/svg/calculators/document_sizing.rb +4 -5
  8. data/lib/prawn/svg/calculators/pixels.rb +27 -3
  9. data/lib/prawn/svg/document.rb +0 -18
  10. data/lib/prawn/svg/elements.rb +2 -2
  11. data/lib/prawn/svg/elements/base.rb +14 -2
  12. data/lib/prawn/svg/elements/circle.rb +1 -1
  13. data/lib/prawn/svg/elements/depth_first_base.rb +52 -0
  14. data/lib/prawn/svg/elements/ellipse.rb +2 -2
  15. data/lib/prawn/svg/elements/image.rb +2 -2
  16. data/lib/prawn/svg/elements/line.rb +4 -4
  17. data/lib/prawn/svg/elements/marker.rb +2 -2
  18. data/lib/prawn/svg/elements/rect.rb +3 -3
  19. data/lib/prawn/svg/elements/root.rb +5 -1
  20. data/lib/prawn/svg/elements/text.rb +47 -85
  21. data/lib/prawn/svg/elements/text_component.rb +186 -0
  22. data/lib/prawn/svg/elements/use.rb +1 -1
  23. data/lib/prawn/svg/elements/viewport.rb +28 -0
  24. data/lib/prawn/svg/interface.rb +20 -12
  25. data/lib/prawn/svg/pathable.rb +18 -2
  26. data/lib/prawn/svg/state.rb +3 -2
  27. data/lib/prawn/svg/version.rb +1 -1
  28. data/spec/prawn/svg/attributes/transform_spec.rb +2 -2
  29. data/spec/prawn/svg/calculators/pixels_spec.rb +72 -0
  30. data/spec/prawn/svg/document_spec.rb +0 -28
  31. data/spec/prawn/svg/elements/base_spec.rb +1 -1
  32. data/spec/prawn/svg/elements/gradient_spec.rb +1 -1
  33. data/spec/prawn/svg/elements/line_spec.rb +1 -1
  34. data/spec/prawn/svg/elements/marker_spec.rb +4 -0
  35. data/spec/prawn/svg/elements/text_spec.rb +104 -15
  36. data/spec/sample_svg/markers_degenerate_cp.svg +19 -0
  37. data/spec/sample_svg/offset_viewport.svg +8 -0
  38. data/spec/sample_svg/subviewports.svg +173 -0
  39. data/spec/sample_svg/subviewports2.svg +16 -0
  40. data/spec/sample_svg/tref01.svg +21 -0
  41. data/spec/sample_svg/tspan05.svg +40 -0
  42. data/spec/sample_svg/tspan91.svg +34 -0
  43. data/spec/spec_helper.rb +6 -0
  44. metadata +22 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3b7ff6138c2cf404469001e4d18217b5758af6c0
4
- data.tar.gz: 15a9474c4bf2f479db7a1e167292c81037d17069
3
+ metadata.gz: bcd6cefab7e10066561afcaee2e9aeed78c9b30f
4
+ data.tar.gz: dbe371b99f2cf4953de90e892a3f95718abdd02e
5
5
  SHA512:
6
- metadata.gz: a446a4b93bc4e590eb76ad93c3b3df2786a300a54c4fd0d28a6e2c1360ea7e7f5ff05ed7759e61cd723c6c16048639d2bc44f6f6be3126d43d59a3259b698465
7
- data.tar.gz: e383d553e521457d5b248f6043086dcf0a9573060c50044e9d631dc5b5e2deff7ed0ad39aaae5c0291f9225608beb038fe488c90390a56a44042c1cd94da72e9
6
+ metadata.gz: 7c567a3f24cebf73c40ec247799f78e730b5f3479be3b486791b7bba9cf7140611db88b00e49422b6bb151613a09296e167e08b30b9f2bd73acf34a9ff49f193
7
+ data.tar.gz: b2e8135844d8e09dbabdf153794775531c6f3b15a6ca796b7a3a765a135b16837fa9127d5d88c616d07709868d296210eba044605540afe010d68e42a63e36c7
data/README.md CHANGED
@@ -60,14 +60,14 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
60
60
  - <tt>&lt;path&gt;</tt> supports all commands defined in SVG 1.1, although the
61
61
  implementation of elliptical arc is a bit rough at the moment.
62
62
 
63
- - <tt>&lt;text&gt;</tt> and <tt>&lt;tspan&gt;</tt> with attributes
64
- <tt>text-anchor</tt>, <tt>font-size</tt>, <tt>font-family</tt>, <tt>font-weight</tt>, <tt>font-style</tt>, <tt>letter-spacing</tt>, <tt>dx</tt>, <tt>dy</tt>
63
+ - `<text>`, `<tspan>` and `<tref>` with attributes `x`, `y`, `dx`, `dy`, `rotate`, and with extra properties
64
+ `text-anchor`, `font-size`, `font-family`, `font-weight`, `font-style`, `letter-spacing`
65
65
 
66
66
  - <tt>&lt;svg&gt;</tt>, <tt>&lt;g&gt;</tt> and <tt>&lt;symbol&gt;</tt>
67
67
 
68
68
  - <tt>&lt;use&gt;</tt>
69
69
 
70
- - <tt>&lt;style&gt;</tt> plus <tt>id</tt>, <tt>class</tt> and <tt>style</tt> attributes (see CSS section below)
70
+ - <tt>&lt;style&gt;</tt> (see CSS section below)
71
71
 
72
72
  - <tt>&lt;image&gt;</tt> with <tt>http:</tt>, <tt>https:</tt>, <tt>data:image/\*;base64</tt> and `file:` schemes
73
73
  (`file:` is disabled by default for security reasons, see Options section above)
@@ -82,19 +82,23 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
82
82
  - `<switch>` and `<foreignObject>`, although prawn-svg cannot handle any data that is not SVG so `<foreignObject>`
83
83
  tags are always ignored.
84
84
 
85
- - attributes/styles: <tt>fill</tt>, <tt>stroke</tt>, <tt>stroke-width</tt>, <tt>stroke-linecap</tt>, <tt>stroke-dasharray</tt>, <tt>opacity</tt>, <tt>fill-opacity</tt>, <tt>stroke-opacity</tt>, <tt>transform</tt>, <tt>clip-path</tt>, <tt>display</tt>, `marker-start`, `marker-mid`, `marker-end`
85
+ - properties: `clip-path`, `color`, `display`, `fill-opacity`, `fill`, `opacity`, `overflow`, `stroke`, `stroke-dasharray`, `stroke-linecap`, `stroke-opacity`, `stroke-width`
86
86
 
87
- - the <tt>viewBox</tt> attribute on <tt>&lt;svg&gt;</tt> and `<marker>` tags
87
+ - properties on lines, polylines, polygons and paths: `marker-end`, `marker-mid`, `marker-start`
88
88
 
89
- - the <tt>preserveAspectRatio</tt> attribute on <tt>&lt;svg&gt;</tt>, <tt>&lt;image&gt;</tt> and `<marker>` tags
89
+ - attributes on all elements: `class`, `id`, `style`, `transform`, `xml:space`
90
90
 
91
- - transform methods: <tt>translate</tt>, <tt>rotate</tt>, <tt>scale</tt>, <tt>matrix</tt>
91
+ - the <tt>viewBox</tt> attribute on <tt>&lt;svg&gt;</tt> and `<marker>` elements
92
+
93
+ - the <tt>preserveAspectRatio</tt> attribute on <tt>&lt;svg&gt;</tt>, <tt>&lt;image&gt;</tt> and `<marker>` elements
94
+
95
+ - transform methods: <tt>translate()</tt>, <tt>rotate()</tt>, <tt>scale()</tt>, <tt>matrix()</tt>
92
96
 
93
97
  - colors: HTML standard names, <tt>#xxx</tt>, <tt>#xxxxxx</tt>, <tt>rgb(1, 2, 3)</tt>, <tt>rgb(1%, 2%, 3%)</tt>
94
98
 
95
99
  - measurements specified in <tt>pt</tt>, <tt>cm</tt>, <tt>dm</tt>, <tt>ft</tt>, <tt>in</tt>, <tt>m</tt>, <tt>mm</tt>, <tt>yd</tt>, <tt>pc</tt>, <tt>%</tt>
96
100
 
97
- - fonts: generic CSS fonts, built-in PDF fonts, and any TTF fonts in your fonts path
101
+ - fonts: generic CSS fonts, built-in PDF fonts, and any TTF fonts in your fonts path, specified in any of the measurements above plus `em` or `rem`
98
102
 
99
103
  ## CSS
100
104
 
@@ -102,7 +106,7 @@ prawn-svg uses the css_parser gem to parse CSS <tt>&lt;style&gt;</tt> blocks. I
102
106
 
103
107
  ## Not supported
104
108
 
105
- prawn-svg does not support sub-viewports, radial gradients, or patterns.
109
+ prawn-svg does not support radial gradients or patterns.
106
110
 
107
111
  ## Configuration
108
112
 
@@ -1,6 +1,6 @@
1
1
  module Prawn::SVG::Attributes
2
2
  end
3
3
 
4
- %w(transform opacity clip_path stroke).each do |name|
4
+ %w(transform opacity clip_path stroke space).each do |name|
5
5
  require "prawn/svg/attributes/#{name}"
6
6
  end
@@ -0,0 +1,10 @@
1
+ module Prawn::SVG::Attributes::Space
2
+ def parse_xml_space_attribute
3
+ case attributes['xml:space']
4
+ when 'preserve'
5
+ state.preserve_space = true
6
+ when 'default'
7
+ state.preserve_space = false
8
+ end
9
+ end
10
+ end
@@ -3,7 +3,7 @@ module Prawn::SVG::Attributes::Stroke
3
3
 
4
4
  def parse_stroke_attributes_and_call
5
5
  if width_string = properties.stroke_width
6
- width = distance(width_string)
6
+ width = pixels(width_string)
7
7
  state.stroke_width = width
8
8
  add_call('line_width', width)
9
9
  end
@@ -21,7 +21,7 @@ module Prawn::SVG::Attributes::Stroke
21
21
  else
22
22
  array = dasharray.split(Prawn::SVG::Elements::COMMA_WSP_REGEXP)
23
23
  array *= 2 if array.length % 2 == 1
24
- number_array = array.map {|value| distance(value)}
24
+ number_array = array.map {|value| pixels(value)}
25
25
 
26
26
  if number_array.any? {|number| number < 0}
27
27
  @document.warnings << "stroke-dasharray cannot have negative numbers; treating as 'none'"
@@ -6,7 +6,7 @@ module Prawn::SVG::Attributes::Transform
6
6
  case name
7
7
  when 'translate'
8
8
  x, y = arguments
9
- add_call_and_enter name, distance(x.to_f, :x), -distance(y.to_f, :y)
9
+ add_call_and_enter name, x_pixels(x.to_f), -y_pixels(y.to_f)
10
10
 
11
11
  when 'rotate'
12
12
  r, x, y = arguments.collect {|a| a.to_f}
@@ -29,7 +29,7 @@ module Prawn::SVG::Attributes::Transform
29
29
  warnings << "transform 'matrix' must have six arguments"
30
30
  else
31
31
  a, b, c, d, e, f = arguments.collect {|argument| argument.to_f}
32
- add_call_and_enter "transformation_matrix", a, -b, -c, d, distance(e, :x), -distance(f, :y)
32
+ add_call_and_enter "transformation_matrix", a, -b, -c, d, x_pixels(e), -y_pixels(f)
33
33
  end
34
34
 
35
35
  else
@@ -30,13 +30,12 @@ module Prawn::SVG::Calculators
30
30
  container_width = @requested_width || @bounds[0]
31
31
  container_height = @requested_height || @bounds[1]
32
32
 
33
- @output_width = Pixels.to_pixels(@document_width || @requested_width, container_width)
34
- @output_height = Pixels.to_pixels(@document_height || @requested_height, container_height)
33
+ @output_width = Pixels::Measurement.to_pixels(@document_width || @requested_width, container_width)
34
+ @output_height = Pixels::Measurement.to_pixels(@document_height || @requested_height, container_height)
35
35
 
36
36
  if @view_box
37
37
  values = @view_box.strip.split(Prawn::SVG::Elements::COMMA_WSP_REGEXP)
38
38
  @x_offset, @y_offset, @viewport_width, @viewport_height = values.map {|value| value.to_f}
39
- @x_offset = -@x_offset
40
39
 
41
40
  if @viewport_width > 0 && @viewport_height > 0
42
41
  @output_width ||= container_width
@@ -49,8 +48,8 @@ module Prawn::SVG::Calculators
49
48
  @y_offset -= aspect.y / @y_scale
50
49
  end
51
50
  else
52
- @output_width ||= Pixels.to_pixels(DEFAULT_WIDTH, container_width)
53
- @output_height ||= Pixels.to_pixels(DEFAULT_HEIGHT, container_height)
51
+ @output_width ||= Pixels::Measurement.to_pixels(DEFAULT_WIDTH, container_width)
52
+ @output_height ||= Pixels::Measurement.to_pixels(DEFAULT_HEIGHT, container_height)
54
53
 
55
54
  @viewport_width = @output_width
56
55
  @viewport_height = @output_height
@@ -1,8 +1,8 @@
1
- module Prawn::SVG::Calculators
2
- class Pixels
1
+ module Prawn::SVG::Calculators::Pixels
2
+ class Measurement
3
3
  extend Prawn::Measurements
4
4
 
5
- def self.to_pixels(value, axis_length)
5
+ def self.to_pixels(value, axis_length = nil)
6
6
  if value.is_a?(String)
7
7
  if match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/)
8
8
  send("#{match[1]}2pt", value.to_f)
@@ -18,4 +18,28 @@ module Prawn::SVG::Calculators
18
18
  end
19
19
  end
20
20
  end
21
+
22
+ protected
23
+
24
+ def x(value)
25
+ x_pixels(value)
26
+ end
27
+
28
+ def y(value)
29
+ # This uses document.sizing, not state.viewport_sizing, because we always
30
+ # want to subtract from the total height of the document.
31
+ document.sizing.output_height - y_pixels(value)
32
+ end
33
+
34
+ def pixels(value)
35
+ value && Measurement.to_pixels(value, state.viewport_sizing.viewport_diagonal)
36
+ end
37
+
38
+ def x_pixels(value)
39
+ value && Measurement.to_pixels(value, state.viewport_sizing.viewport_width)
40
+ end
41
+
42
+ def y_pixels(value)
43
+ value && Measurement.to_pixels(value, state.viewport_sizing.viewport_height)
44
+ end
21
45
  end
@@ -43,26 +43,8 @@ class Prawn::SVG::Document
43
43
  sizing.requested_height = options[:height]
44
44
  sizing.calculate
45
45
 
46
- @axis_to_size = {:x => sizing.viewport_width, :y => sizing.viewport_height}
47
-
48
46
  @css_parser = CssParser::Parser.new
49
47
 
50
48
  yield self if block_given?
51
49
  end
52
-
53
- def x(value)
54
- points(value, :x)
55
- end
56
-
57
- def y(value)
58
- sizing.output_height - points(value, :y)
59
- end
60
-
61
- def distance(value, axis = nil)
62
- value && points(value, axis)
63
- end
64
-
65
- def points(value, axis = nil)
66
- Prawn::SVG::Calculators::Pixels.to_pixels(value, @axis_to_size.fetch(axis, sizing.viewport_diagonal))
67
- end
68
50
  end
@@ -2,18 +2,18 @@ module Prawn::SVG::Elements
2
2
  COMMA_WSP_REGEXP = /(?:\s+,?\s*|,\s*)/
3
3
  end
4
4
 
5
- %w(base root container style text line polyline polygon circle ellipse rect path use image gradient marker ignored).each do |filename|
5
+ %w(base depth_first_base root container viewport style text text_component line polyline polygon circle ellipse rect path use image gradient marker ignored).each do |filename|
6
6
  require "prawn/svg/elements/#{filename}"
7
7
  end
8
8
 
9
9
  module Prawn::SVG::Elements
10
10
  TAG_CLASS_MAPPING = {
11
- svg: Prawn::SVG::Elements::Container,
12
11
  g: Prawn::SVG::Elements::Container,
13
12
  symbol: Prawn::SVG::Elements::Container,
14
13
  defs: Prawn::SVG::Elements::Container,
15
14
  clipPath: Prawn::SVG::Elements::Container,
16
15
  switch: Prawn::SVG::Elements::Container,
16
+ svg: Prawn::SVG::Elements::Viewport,
17
17
  style: Prawn::SVG::Elements::Style,
18
18
  text: Prawn::SVG::Elements::Text,
19
19
  line: Prawn::SVG::Elements::Line,
@@ -1,10 +1,13 @@
1
1
  class Prawn::SVG::Elements::Base
2
2
  extend Forwardable
3
3
 
4
+ include Prawn::SVG::Calculators::Pixels
5
+
4
6
  include Prawn::SVG::Attributes::Transform
5
7
  include Prawn::SVG::Attributes::Opacity
6
8
  include Prawn::SVG::Attributes::ClipPath
7
9
  include Prawn::SVG::Attributes::Stroke
10
+ include Prawn::SVG::Attributes::Space
8
11
 
9
12
  PAINT_TYPES = %w(fill stroke)
10
13
  COMMA_WSP_REGEXP = Prawn::SVG::Elements::COMMA_WSP_REGEXP
@@ -16,7 +19,7 @@ class Prawn::SVG::Elements::Base
16
19
  attr_reader :document, :source, :parent_calls, :base_calls, :state, :attributes, :properties
17
20
  attr_accessor :calls
18
21
 
19
- def_delegators :@document, :x, :y, :distance, :points, :warnings
22
+ def_delegators :@document, :warnings
20
23
  def_delegator :@state, :computed_properties
21
24
 
22
25
  def initialize(document, source, parent_calls, state)
@@ -39,6 +42,7 @@ class Prawn::SVG::Elements::Base
39
42
  end
40
43
 
41
44
  def parse_and_apply
45
+ parse_standard_attributes
42
46
  parse
43
47
 
44
48
  apply_calls_from_standard_attributes
@@ -71,6 +75,14 @@ class Prawn::SVG::Elements::Base
71
75
  false
72
76
  end
73
77
 
78
+ def drawable?
79
+ !container?
80
+ end
81
+
82
+ def parse_standard_attributes
83
+ parse_xml_space_attribute
84
+ end
85
+
74
86
  def add_call(name, *arguments)
75
87
  @calls << [name.to_s, arguments, []]
76
88
  end
@@ -131,7 +143,7 @@ class Prawn::SVG::Elements::Base
131
143
  end
132
144
 
133
145
  def apply_drawing_call
134
- if !state.disable_drawing && !container?
146
+ if !state.disable_drawing && drawable?
135
147
  draw_types = PAINT_TYPES.select { |property| computed_properties.send(property) != 'none' }
136
148
 
137
149
  if draw_types.empty?
@@ -6,7 +6,7 @@ class Prawn::SVG::Elements::Circle < Prawn::SVG::Elements::Base
6
6
 
7
7
  @x = x(attributes['cx'] || "0")
8
8
  @y = y(attributes['cy'] || "0")
9
- @r = distance(attributes['r'])
9
+ @r = pixels(attributes['r'])
10
10
 
11
11
  require_positive_value @r
12
12
  end
@@ -0,0 +1,52 @@
1
+ class Prawn::SVG::Elements::DepthFirstBase < Prawn::SVG::Elements::Base
2
+ def initialize(document, source, parent_calls, state)
3
+ super
4
+ @base_calls = @calls = @parent_calls
5
+ end
6
+
7
+ def process
8
+ parse_step
9
+ apply_step(calls)
10
+ rescue SkipElementQuietly
11
+ rescue SkipElementError => e
12
+ @document.warnings << e.message
13
+ end
14
+
15
+ def parse_and_apply
16
+ raise "unsupported"
17
+ end
18
+
19
+ protected
20
+
21
+ def parse_step
22
+ extract_attributes_and_properties
23
+ parse_standard_attributes
24
+ parse
25
+ parse_child_elements if container?
26
+ end
27
+
28
+ def apply_step(calls)
29
+ @base_calls = @calls = calls
30
+ apply_calls_from_standard_attributes
31
+ apply
32
+ apply_child_elements if container?
33
+ end
34
+
35
+ def parse_child_elements
36
+ return unless source
37
+
38
+ source.elements.each do |elem|
39
+ if element_class = Prawn::SVG::Elements::TAG_CLASS_MAPPING[elem.name.to_sym]
40
+ child = element_class.new(@document, elem, @calls, state.dup)
41
+ child.parse_step
42
+ @children << child
43
+ end
44
+ end
45
+ end
46
+
47
+ def apply_child_elements
48
+ @children.each do |child|
49
+ child.apply_step(calls)
50
+ end
51
+ end
52
+ end
@@ -6,8 +6,8 @@ class Prawn::SVG::Elements::Ellipse < Prawn::SVG::Elements::Base
6
6
 
7
7
  @x = x(attributes['cx'] || "0")
8
8
  @y = y(attributes['cy'] || "0")
9
- @rx = distance(attributes['rx'], :x)
10
- @ry = distance(attributes['ry'], :y)
9
+ @rx = x_pixels(attributes['rx'])
10
+ @ry = y_pixels(attributes['ry'])
11
11
 
12
12
  require_positive_value @rx, @ry
13
13
  end
@@ -22,8 +22,8 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
22
22
 
23
23
  x = x(attributes['x'] || 0)
24
24
  y = y(attributes['y'] || 0)
25
- width = distance(attributes['width'])
26
- height = distance(attributes['height'])
25
+ width = x_pixels(attributes['width'])
26
+ height = y_pixels(attributes['height'])
27
27
 
28
28
  raise SkipElementQuietly if width.zero? || height.zero?
29
29
  require_positive_value width, height
@@ -2,10 +2,10 @@ class Prawn::SVG::Elements::Line < Prawn::SVG::Elements::Base
2
2
  include Prawn::SVG::Pathable
3
3
 
4
4
  def parse
5
- @x1 = points(attributes['x1'] || '0', :x)
6
- @y1 = points(attributes['y1'] || '0', :y)
7
- @x2 = points(attributes['x2'] || '0', :x)
8
- @y2 = points(attributes['y2'] || '0', :y)
5
+ @x1 = x_pixels(attributes['x1'] || 0)
6
+ @y1 = y_pixels(attributes['y1'] || 0)
7
+ @x2 = x_pixels(attributes['x2'] || 0)
8
+ @y2 = y_pixels(attributes['y2'] || 0)
9
9
  end
10
10
 
11
11
  def apply
@@ -41,8 +41,8 @@ class Prawn::SVG::Elements::Marker < Prawn::SVG::Elements::Base
41
41
  element.add_call 'transformation_matrix', scale, 0, 0, scale, 0, 0
42
42
  end
43
43
 
44
- ref_x = document.distance(attributes['refX']) || 0
45
- ref_y = document.distance(attributes['refY']) || 0
44
+ ref_x = x_pixels(attributes['refX']) || 0
45
+ ref_y = y_pixels(attributes['refY']) || 0
46
46
 
47
47
  element.add_call 'transformation_matrix', 1, 0, 0, 1, -ref_x * sizing.x_scale, ref_y * sizing.y_scale
48
48
 
@@ -4,12 +4,12 @@ class Prawn::SVG::Elements::Rect < Prawn::SVG::Elements::Base
4
4
 
5
5
  @x = x(attributes['x'] || '0')
6
6
  @y = y(attributes['y'] || '0')
7
- @width = distance(attributes['width'], :x)
8
- @height = distance(attributes['height'], :y)
7
+ @width = x_pixels(attributes['width'])
8
+ @height = y_pixels(attributes['height'])
9
9
 
10
10
  require_positive_value @width, @height
11
11
 
12
- @radius = distance(attributes['rx'] || attributes['ry'])
12
+ @radius = x_pixels(attributes['rx']) || y_pixels(attributes['ry'])
13
13
  if @radius
14
14
  # If you implement separate rx and ry in the future, you'll want to change this
15
15
  # so that rx is constrained to @width/2 and ry is constrained to @height/2.
@@ -3,10 +3,14 @@ class Prawn::SVG::Elements::Root < Prawn::SVG::Elements::Base
3
3
  super
4
4
  end
5
5
 
6
+ def parse
7
+ state.viewport_sizing = @document.sizing
8
+ end
9
+
6
10
  def apply
7
11
  add_call 'fill_color', '000000'
8
12
  add_call 'transformation_matrix', @document.sizing.x_scale, 0, 0, @document.sizing.y_scale, 0, 0
9
- add_call 'transformation_matrix', 1, 0, 0, 1, @document.sizing.x_offset, @document.sizing.y_offset
13
+ add_call 'transformation_matrix', 1, 0, 0, 1, -@document.sizing.x_offset, @document.sizing.y_offset
10
14
  end
11
15
 
12
16
  def container?
@@ -1,108 +1,70 @@
1
- class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
1
+ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::DepthFirstBase
2
2
  def parse
3
- case attributes['xml:space']
4
- when 'preserve'
5
- state.preserve_space = true
6
- when 'default'
7
- state.preserve_space = false
8
- end
9
-
10
- @relative = state.text_relative || false
3
+ state.text = Prawn::SVG::Elements::TextComponent::PositionsList.new([], [], [], [], [], nil)
11
4
 
12
- if attributes['x'] || attributes['y']
13
- @relative = false
14
- @x_positions = attributes['x'].split(COMMA_WSP_REGEXP).collect {|n| document.x(n)} if attributes['x']
15
- @y_positions = attributes['y'].split(COMMA_WSP_REGEXP).collect {|n| document.y(n)} if attributes['y']
16
- end
5
+ @text_root = Prawn::SVG::Elements::TextComponent.new(document, source, nil, state.dup)
6
+ @text_root.parse_step
17
7
 
18
- @x_positions ||= state.text_x_positions || [document.x(0)]
19
- @y_positions ||= state.text_y_positions || [document.y(0)]
8
+ reintroduce_trailing_and_leading_whitespace
20
9
  end
21
10
 
22
11
  def apply
23
- raise SkipElementQuietly if computed_properties.display == "none"
24
-
25
- font = select_font
26
- apply_font(font) if font
27
-
28
- add_call_and_enter "text_group" if name == 'text'
29
-
30
- if attributes['dx'] || attributes['dy']
31
- add_call_and_enter "translate", document.distance(attributes['dx'] || 0), -document.distance(attributes['dy'] || 0)
32
- end
33
-
34
- # text_anchor isn't a Prawn option; we have to do some math to support it
35
- # and so we handle this in Prawn::SVG::Interface#rewrite_call_arguments
36
- opts = {
37
- size: computed_properties.numerical_font_size,
38
- style: font && font.subfamily,
39
- text_anchor: computed_properties.text_anchor
40
- }
41
-
42
- spacing = computed_properties.letter_spacing
43
- spacing = spacing == 'normal' ? 0 : document.points(spacing)
44
-
45
- add_call_and_enter 'character_spacing', spacing
46
-
47
- source.children.each do |child|
48
- if child.node_type == :text
49
- text = child.value.strip.gsub(state.preserve_space ? /[\n\t]/ : /\s+/, " ")
50
-
51
- while text != ""
52
- opts[:at] = [@x_positions.first, @y_positions.first]
53
-
54
- if @x_positions.length > 1 || @y_positions.length > 1
55
- add_call 'draw_text', text[0..0], opts.dup
56
- text = text[1..-1]
12
+ add_call_and_enter "text_group"
13
+ @text_root.apply_step(calls)
14
+ end
57
15
 
58
- @x_positions.shift if @x_positions.length > 1
59
- @y_positions.shift if @y_positions.length > 1
60
- else
61
- add_call @relative ? 'relative_draw_text' : 'draw_text', text, opts.dup
62
- @relative = true
63
- break
64
- end
65
- end
16
+ private
66
17
 
67
- elsif child.name == "tspan"
68
- add_call 'save'
18
+ def drawable?
19
+ false
20
+ end
69
21
 
70
- new_state = state.dup
71
- new_state.text_x_positions = @x_positions
72
- new_state.text_y_positions = @y_positions
73
- new_state.text_relative = @relative
22
+ def apply_calls_from_standard_attributes
23
+ # overridden because we want the attributes to be applied in the TextComponent root,
24
+ # which is a duplicate of this element.
25
+ end
74
26
 
75
- Prawn::SVG::Elements::Text.new(document, child, calls, new_state).process
27
+ def reintroduce_trailing_and_leading_whitespace
28
+ printables = []
29
+ built_printable_queue(printables, @text_root)
76
30
 
77
- add_call 'restore'
31
+ remove_whitespace_only_printables_and_start_and_end(printables)
32
+ remove_printables_that_are_completely_empty(printables)
33
+ apportion_leading_and_trailing_spaces(printables)
34
+ end
78
35
 
36
+ def built_printable_queue(queue, component)
37
+ component.commands.each do |command|
38
+ case command
39
+ when Prawn::SVG::Elements::TextComponent::Printable
40
+ queue << command
79
41
  else
80
- warnings << "Unknown tag '#{child.name}' inside text tag; ignoring"
42
+ built_printable_queue(queue, command)
81
43
  end
82
44
  end
83
-
84
- # It's possible there was no text to render. In that case, add a 'noop' so
85
- # character_spacing doesn't blow up when it finds it doesn't have a block to execute.
86
- add_call 'noop' if calls.empty?
87
45
  end
88
46
 
89
- private
90
-
91
- def select_font
92
- font_families = [computed_properties.font_family, document.fallback_font_name]
93
- font_style = :italic if computed_properties.font_style == 'italic'
94
- font_weight = Prawn::SVG::Font.weight_for_css_font_weight(computed_properties.font_weight)
47
+ def remove_whitespace_only_printables_and_start_and_end(printables)
48
+ printables.pop while printables.last && printables.last.text.empty?
49
+ printables.shift while printables.first && printables.first.text.empty?
50
+ end
95
51
 
96
- font_families.compact.each do |name|
97
- font = document.font_registry.load(name, font_weight, font_style)
98
- return font if font
52
+ def remove_printables_that_are_completely_empty(printables)
53
+ printables.reject! do |printable|
54
+ printable.text.empty? && !printable.trailing_space? && !printable.leading_space?
99
55
  end
100
-
101
- warnings << "Font family '#{computed_properties.font_family}' style '#{computed_properties.font_style}' is not a known font, and the fallback font could not be found."
102
- nil
103
56
  end
104
57
 
105
- def apply_font(font)
106
- add_call 'font', font.name, style: font.subfamily
58
+ def apportion_leading_and_trailing_spaces(printables)
59
+ printables.each_cons(2) do |a, b|
60
+ if a.text.empty?
61
+ # Empty strings can only get a leading space from the previous non-empty text,
62
+ # and never get a trailing space
63
+ elsif a.trailing_space?
64
+ a.text += ' '
65
+ elsif b.leading_space?
66
+ b.text = " #{b.text}"
67
+ end
68
+ end
107
69
  end
108
70
  end