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
@@ -0,0 +1,186 @@
1
+ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
2
+ attr_reader :commands
3
+
4
+ Printable = Struct.new(:element, :text, :leading_space?, :trailing_space?)
5
+ PositionsList = Struct.new(:x, :y, :dx, :dy, :rotation, :parent)
6
+
7
+ def parse
8
+ state.text.x = attributes['x'].split(COMMA_WSP_REGEXP).collect {|n| x(n)} if attributes['x']
9
+ state.text.y = attributes['y'].split(COMMA_WSP_REGEXP).collect {|n| y(n)} if attributes['y']
10
+ state.text.dx = attributes['dx'].split(COMMA_WSP_REGEXP).collect {|n| x_pixels(n)} if attributes['dx']
11
+ state.text.dy = attributes['dy'].split(COMMA_WSP_REGEXP).collect {|n| y_pixels(n)} if attributes['dy']
12
+ state.text.rotation = attributes['rotate'].split(COMMA_WSP_REGEXP).collect(&:to_f) if attributes['rotate']
13
+
14
+ @commands = []
15
+
16
+ text_children.each do |child|
17
+ if child.node_type == :text
18
+ append_text(child)
19
+ else
20
+ case child.name
21
+ when 'tspan', 'tref'
22
+ append_child(child)
23
+ else
24
+ warnings << "Unknown tag '#{child.name}' inside text tag; ignoring"
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def apply
31
+ raise SkipElementQuietly if computed_properties.display == "none"
32
+
33
+ font = select_font
34
+ apply_font(font) if font
35
+
36
+ # text_anchor isn't a Prawn option; we have to do some math to support it
37
+ # and so we handle this in Prawn::SVG::Interface#rewrite_call_arguments
38
+ opts = {
39
+ size: computed_properties.numerical_font_size,
40
+ style: font && font.subfamily,
41
+ text_anchor: computed_properties.text_anchor
42
+ }
43
+
44
+ spacing = computed_properties.letter_spacing
45
+ spacing = spacing == 'normal' ? 0 : pixels(spacing)
46
+
47
+ add_call_and_enter 'character_spacing', spacing
48
+
49
+ @commands.each do |command|
50
+ case command
51
+ when Printable
52
+ apply_text(command.text, opts)
53
+ when self.class
54
+ add_call 'save'
55
+ command.apply_step(calls)
56
+ add_call 'restore'
57
+ else
58
+ raise
59
+ end
60
+ end
61
+
62
+ # It's possible there was no text to render. In that case, add a 'noop' so
63
+ # character_spacing doesn't blow up when it finds it doesn't have a block to execute.
64
+ add_call 'noop' if calls.empty?
65
+ end
66
+
67
+ protected
68
+
69
+ def append_text(child)
70
+ if state.preserve_space
71
+ text = child.value.tr("\n\t", ' ')
72
+ else
73
+ text = child.value.tr("\n", '').tr("\t", ' ')
74
+ leading = text[0] == ' '
75
+ trailing = text[-1] == ' '
76
+ text = text.strip.gsub(/ {2,}/, ' ')
77
+ end
78
+
79
+ @commands << Printable.new(self, text, leading, trailing)
80
+ end
81
+
82
+ def append_child(child)
83
+ new_state = state.dup
84
+ new_state.text = PositionsList.new([], [], [], [], [], state.text)
85
+
86
+ element = self.class.new(document, child, calls, new_state)
87
+ @commands << element
88
+ element.parse_step
89
+ end
90
+
91
+ def apply_text(text, opts)
92
+ while text != ""
93
+ x = y = dx = dy = rotate = nil
94
+ remaining = rotation_remaining = false
95
+
96
+ list = state.text
97
+ while list
98
+ shifted = list.x.shift
99
+ x ||= shifted
100
+ shifted = list.y.shift
101
+ y ||= shifted
102
+ shifted = list.dx.shift
103
+ dx ||= shifted
104
+ shifted = list.dy.shift
105
+ dy ||= shifted
106
+
107
+ shifted = list.rotation.length > 1 ? list.rotation.shift : list.rotation.first
108
+ if shifted && rotate.nil?
109
+ rotate = shifted
110
+ remaining ||= list.rotation != [0]
111
+ end
112
+
113
+ remaining ||= list.x.any? || list.y.any? || list.dx.any? || list.dy.any? || (rotate && rotate != 0)
114
+ rotation_remaining ||= list.rotation.length > 1
115
+ list = list.parent
116
+ end
117
+
118
+ opts[:at] = [x || :relative, y || :relative]
119
+ opts[:offset] = [dx || 0, dy || 0]
120
+
121
+ if rotate && rotate != 0
122
+ opts[:rotate] = -rotate
123
+ else
124
+ opts.delete(:rotate)
125
+ end
126
+
127
+ if remaining
128
+ add_call 'draw_text', text[0..0], opts.dup
129
+ text = text[1..-1]
130
+ else
131
+ add_call 'draw_text', text, opts.dup
132
+
133
+ # we can get to this path with rotations still pending
134
+ # solve this by shifting them out by the number of
135
+ # characters we've just drawn
136
+ shift = text.length - 1
137
+ if rotation_remaining && shift > 0
138
+ list = state.text
139
+ while list
140
+ count = [shift, list.rotation.length - 1].min
141
+ list.rotation.shift(count) if count > 0
142
+ list = list.parent
143
+ end
144
+ end
145
+
146
+ break
147
+ end
148
+ end
149
+ end
150
+
151
+ def text_children
152
+ if name == 'tref'
153
+ reference = find_referenced_element
154
+ reference ? reference.source.children : []
155
+ else
156
+ source.children
157
+ end
158
+ end
159
+
160
+ def find_referenced_element
161
+ href = attributes['xlink:href']
162
+
163
+ if href && href[0..0] == '#'
164
+ element = document.elements_by_id[href[1..-1]]
165
+ element if element.name == 'text'
166
+ end
167
+ end
168
+
169
+ def select_font
170
+ font_families = [computed_properties.font_family, document.fallback_font_name]
171
+ font_style = :italic if computed_properties.font_style == 'italic'
172
+ font_weight = Prawn::SVG::Font.weight_for_css_font_weight(computed_properties.font_weight)
173
+
174
+ font_families.compact.each do |name|
175
+ font = document.font_registry.load(name, font_weight, font_style)
176
+ return font if font
177
+ end
178
+
179
+ 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."
180
+ nil
181
+ end
182
+
183
+ def apply_font(font)
184
+ add_call 'font', font.name, style: font.subfamily
185
+ end
186
+ end
@@ -21,7 +21,7 @@ class Prawn::SVG::Elements::Use < Prawn::SVG::Elements::Base
21
21
 
22
22
  def apply
23
23
  if @x || @y
24
- add_call_and_enter "translate", distance(@x || 0, :x), -distance(@y || 0, :y)
24
+ add_call_and_enter "translate", x_pixels(@x || 0), -y_pixels(@y || 0)
25
25
  end
26
26
 
27
27
  add_calls_from_element @definition_element
@@ -0,0 +1,28 @@
1
+ class Prawn::SVG::Elements::Viewport < Prawn::SVG::Elements::Base
2
+ def parse
3
+ parent_dimensions = [state.viewport_sizing.viewport_width, state.viewport_sizing.viewport_height]
4
+
5
+ @sizing = Prawn::SVG::Calculators::DocumentSizing.new(parent_dimensions, attributes)
6
+ @sizing.calculate
7
+
8
+ @x = x_pixels(attributes['x'] || 0)
9
+ @y = y_pixels(attributes['y'] || 0)
10
+
11
+ state.viewport_sizing = @sizing
12
+ end
13
+
14
+ def apply
15
+ if @x != 0 || @y != 0
16
+ add_call 'transformation_matrix', 1, 0, 0, 1, @x, -@y
17
+ end
18
+
19
+ add_call 'rectangle', [0, y(0)], @sizing.output_width, @sizing.output_height
20
+ add_call 'clip'
21
+ add_call 'transformation_matrix', @sizing.x_scale, 0, 0, @sizing.y_scale, 0, 0
22
+ add_call 'transformation_matrix', 1, 0, 0, 1, -@sizing.x_offset, @sizing.y_offset
23
+ end
24
+
25
+ def container?
26
+ true
27
+ end
28
+ end
@@ -118,28 +118,36 @@ module Prawn
118
118
  end
119
119
 
120
120
  def rewrite_call_arguments(prawn, call, arguments)
121
- if call == 'relative_draw_text'
122
- call.replace "draw_text"
123
- arguments.last[:at][0] = @relative_text_position if @relative_text_position
124
- end
125
-
126
121
  case call
127
122
  when 'text_group'
128
- @relative_text_position = nil
123
+ @cursor = [0, document.sizing.output_height]
129
124
  yield
130
125
 
131
126
  when 'draw_text'
132
127
  text, options = arguments
133
128
 
134
- width = prawn.width_of(text, options.merge(:kerning => true))
129
+ at = options.fetch(:at)
130
+
131
+ at[0] = @cursor[0] if at[0] == :relative
132
+ at[1] = @cursor[1] if at[1] == :relative
135
133
 
136
- if (anchor = options.delete(:text_anchor)) && %w(middle end).include?(anchor)
137
- width /= 2 if anchor == 'middle'
138
- options[:at][0] -= width
134
+ if offset = options.delete(:offset)
135
+ at[0] += offset[0]
136
+ at[1] -= offset[1]
139
137
  end
140
138
 
141
- space_width = prawn.width_of("n", options)
142
- @relative_text_position = options[:at][0] + width + space_width
139
+ width = prawn.width_of(text, options.merge(kerning: true))
140
+
141
+ case options.delete(:text_anchor)
142
+ when 'middle'
143
+ at[0] -= width / 2
144
+ @cursor = [at[0] + width / 2, at[1]]
145
+ when 'end'
146
+ at[0] -= width
147
+ @cursor = at.dup
148
+ else
149
+ @cursor = [at[0] + width, at[1]]
150
+ end
143
151
 
144
152
  when 'transformation_matrix'
145
153
  left = prawn.bounds.absolute_left
@@ -60,8 +60,12 @@ module Prawn::SVG::Pathable
60
60
  angle = Math.atan2(command.destination[1] - last_point[1], command.destination[0] - last_point[0]) * 180.0 / Math::PI
61
61
  [angle, angle]
62
62
  when Curve
63
- start = Math.atan2(command.point1[1] - last_point[1], command.point1[0] - last_point[0]) * 180.0 / Math::PI
64
- stop = Math.atan2(command.destination[1] - command.point2[1], command.destination[0] - command.point2[0]) * 180.0 / Math::PI
63
+ point = select_non_equal_point(last_point, command.point1, command.point2, command.destination)
64
+ start = Math.atan2(point[1] - last_point[1], point[0] - last_point[0]) * 180.0 / Math::PI
65
+
66
+ point = select_non_equal_point(command.destination, command.point2, command.point1, last_point)
67
+ stop = Math.atan2(command.destination[1] - point[1], command.destination[0] - point[0]) * 180.0 / Math::PI
68
+
65
69
  [start, stop]
66
70
  else
67
71
  raise NotImplementedError, "Unknown path command type"
@@ -127,4 +131,16 @@ module Prawn::SVG::Pathable
127
131
  def translate(point)
128
132
  [point[0].to_f, document.sizing.output_height - point[1].to_f]
129
133
  end
134
+
135
+ private
136
+
137
+ def select_non_equal_point(base, point_a, point_b, point_c)
138
+ if point_a != base
139
+ point_a
140
+ elsif point_b != base
141
+ point_b
142
+ else
143
+ point_c
144
+ end
145
+ end
130
146
  end
@@ -1,8 +1,9 @@
1
1
  class Prawn::SVG::State
2
2
  attr_accessor :disable_drawing,
3
- :text_relative, :text_x_positions, :text_y_positions, :preserve_space,
3
+ :text, :preserve_space,
4
4
  :fill_opacity, :stroke_opacity, :stroke_width,
5
- :computed_properties
5
+ :computed_properties,
6
+ :viewport_sizing
6
7
 
7
8
  def initialize
8
9
  @stroke_width = 1
@@ -1,5 +1,5 @@
1
1
  module Prawn
2
2
  module SVG
3
- VERSION = '0.24.0'
3
+ VERSION = '0.25.0'
4
4
  end
5
5
  end
@@ -20,8 +20,8 @@ describe Prawn::SVG::Attributes::Transform do
20
20
  describe "translate" do
21
21
  it "handles a missing y argument" do
22
22
  expect(element).to receive(:add_call_and_enter).with('translate', -5.5, 0)
23
- expect(element).to receive(:distance).with(-5.5, :x).and_return(-5.5)
24
- expect(element).to receive(:distance).with(0.0, :y).and_return(0.0)
23
+ expect(element).to receive(:x_pixels).with(-5.5).and_return(-5.5)
24
+ expect(element).to receive(:y_pixels).with(0.0).and_return(0.0)
25
25
 
26
26
  element.attributes['transform'] = 'translate(-5.5)'
27
27
  subject
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe Prawn::SVG::Calculators::Pixels do
4
+ class TestPixelsCalculator
5
+ include Prawn::SVG::Calculators::Pixels
6
+
7
+ [:x, :y, :pixels, :x_pixels, :y_pixels].each { |method| public method }
8
+ end
9
+
10
+ let(:viewport_sizing) do
11
+ instance_double(Prawn::SVG::Calculators::DocumentSizing, viewport_width: 600, viewport_height: 400, viewport_diagonal: 500, :requested_width= => nil, :requested_height= => nil)
12
+ end
13
+
14
+ let(:document_sizing) do
15
+ instance_double(Prawn::SVG::Calculators::DocumentSizing, output_height: 800)
16
+ end
17
+
18
+ let(:state) { instance_double(Prawn::SVG::State, viewport_sizing: viewport_sizing) }
19
+ let(:document) { instance_double(Prawn::SVG::Document, sizing: document_sizing) }
20
+
21
+ subject { TestPixelsCalculator.new }
22
+
23
+ before do
24
+ allow(subject).to receive(:state).and_return(state)
25
+ allow(subject).to receive(:document).and_return(document)
26
+ end
27
+
28
+ describe "#pixels" do
29
+ it "converts a variety of measurement units to points" do
30
+ expect(subject.pixels(32)).to eq 32.0
31
+ expect(subject.pixels(32.0)).to eq 32.0
32
+ expect(subject.pixels("32")).to eq 32.0
33
+ expect(subject.pixels("32unknown")).to eq 32.0
34
+ expect(subject.pixels("32pt")).to eq 32.0
35
+ expect(subject.pixels("32in")).to eq 32.0 * 72
36
+ expect(subject.pixels("32ft")).to eq 32.0 * 72 * 12
37
+ expect(subject.pixels("32pc")).to eq 32.0 * 15
38
+ expect(subject.pixels("32mm")).to be_within(0.0001).of(32 * 72 * 0.0393700787)
39
+ expect(subject.pixels("32cm")).to be_within(0.0001).of(32 * 72 * 0.393700787)
40
+ expect(subject.pixels("32m")).to be_within(0.0001).of(32 * 72 * 39.3700787)
41
+ expect(subject.pixels("50%")).to eq 250
42
+ end
43
+ end
44
+
45
+ describe "#x_pixels" do
46
+ it "uses the viewport width for percentages" do
47
+ expect(subject.x_pixels("50")).to eq 50
48
+ expect(subject.x_pixels("50%")).to eq 300
49
+ end
50
+ end
51
+
52
+ describe "#y_pixels" do
53
+ it "uses the viewport height for percentages" do
54
+ expect(subject.y_pixels("50")).to eq 50
55
+ expect(subject.y_pixels("50%")).to eq 200
56
+ end
57
+ end
58
+
59
+ describe "#x" do
60
+ it "performs the same as #x_pixels" do
61
+ expect(subject.x("50")).to eq 50
62
+ expect(subject.x("50%")).to eq 300
63
+ end
64
+ end
65
+
66
+ describe "#y" do
67
+ it "performs the same as #y_pixels but subtracts the pixels from the page height" do
68
+ expect(subject.y("50")).to eq 800 - 50
69
+ expect(subject.y("50%")).to eq 800 - 200
70
+ end
71
+ end
72
+ end
@@ -25,32 +25,4 @@ describe Prawn::SVG::Document do
25
25
  end
26
26
  end
27
27
  end
28
-
29
- describe "#points" do
30
- before do
31
- sizing = instance_double(Prawn::SVG::Calculators::DocumentSizing, viewport_width: 600, viewport_height: 400, viewport_diagonal: 500, :requested_width= => nil, :requested_height= => nil)
32
- expect(sizing).to receive(:calculate)
33
- expect(Prawn::SVG::Calculators::DocumentSizing).to receive(:new).and_return(sizing)
34
- end
35
-
36
- let(:document) { Prawn::SVG::Document.new("<svg></svg>", [100, 100], {}) }
37
-
38
- it "converts a variety of measurement units to points" do
39
- document.send(:points, 32).should == 32.0
40
- document.send(:points, 32.0).should == 32.0
41
- document.send(:points, "32").should == 32.0
42
- document.send(:points, "32unknown").should == 32.0
43
- document.send(:points, "32pt").should == 32.0
44
- document.send(:points, "32in").should == 32.0 * 72
45
- document.send(:points, "32ft").should == 32.0 * 72 * 12
46
- document.send(:points, "32pc").should == 32.0 * 15
47
- document.send(:points, "32mm").should be_within(0.0001).of(32 * 72 * 0.0393700787)
48
- document.send(:points, "32cm").should be_within(0.0001).of(32 * 72 * 0.393700787)
49
- document.send(:points, "32m").should be_within(0.0001).of(32 * 72 * 39.3700787)
50
-
51
- document.send(:points, "50%").should == 250
52
- document.send(:points, "50%", :x).should == 300
53
- document.send(:points, "50%", :y).should == 200
54
- end
55
- end
56
28
  end
@@ -4,7 +4,7 @@ describe Prawn::SVG::Elements::Base do
4
4
  let(:svg) { "<svg></svg>" }
5
5
  let(:document) { Prawn::SVG::Document.new(svg, [800, 600], {}, font_registry: Prawn::SVG::FontRegistry.new("Helvetica" => {:normal => nil})) }
6
6
  let(:parent_calls) { [] }
7
- let(:element) { Prawn::SVG::Elements::Base.new(document, document.root, parent_calls, Prawn::SVG::State.new) }
7
+ let(:element) { Prawn::SVG::Elements::Base.new(document, document.root, parent_calls, fake_state) }
8
8
 
9
9
  describe "#initialize" do
10
10
  let(:svg) { '<something id="hello"/>' }