prawn-svg 0.24.0 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
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"/>' }