prawn-svg 0.21.0 → 0.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -0
  3. data/README.md +11 -4
  4. data/lib/prawn-svg.rb +9 -6
  5. data/lib/prawn/svg/attributes.rb +6 -0
  6. data/lib/prawn/svg/attributes/clip_path.rb +17 -0
  7. data/lib/prawn/svg/attributes/display.rb +5 -0
  8. data/lib/prawn/svg/attributes/font.rb +38 -0
  9. data/lib/prawn/svg/attributes/opacity.rb +15 -0
  10. data/lib/prawn/svg/attributes/stroke.rb +35 -0
  11. data/lib/prawn/svg/attributes/transform.rb +50 -0
  12. data/lib/prawn/svg/calculators/aspect_ratio.rb +1 -1
  13. data/lib/prawn/svg/calculators/document_sizing.rb +2 -2
  14. data/lib/prawn/svg/calculators/pixels.rb +1 -1
  15. data/lib/prawn/svg/color.rb +44 -14
  16. data/lib/prawn/svg/document.rb +6 -5
  17. data/lib/prawn/svg/elements.rb +33 -0
  18. data/lib/prawn/svg/elements/base.rb +228 -0
  19. data/lib/prawn/svg/elements/circle.rb +25 -0
  20. data/lib/prawn/svg/elements/container.rb +15 -0
  21. data/lib/prawn/svg/elements/ellipse.rb +23 -0
  22. data/lib/prawn/svg/elements/gradient.rb +117 -0
  23. data/lib/prawn/svg/elements/ignored.rb +5 -0
  24. data/lib/prawn/svg/elements/image.rb +85 -0
  25. data/lib/prawn/svg/elements/line.rb +16 -0
  26. data/lib/prawn/svg/elements/path.rb +405 -0
  27. data/lib/prawn/svg/elements/polygon.rb +17 -0
  28. data/lib/prawn/svg/elements/polyline.rb +22 -0
  29. data/lib/prawn/svg/elements/rect.rb +33 -0
  30. data/lib/prawn/svg/elements/root.rb +9 -0
  31. data/lib/prawn/svg/elements/style.rb +10 -0
  32. data/lib/prawn/svg/elements/text.rb +87 -0
  33. data/lib/prawn/svg/elements/use.rb +29 -0
  34. data/lib/prawn/svg/extension.rb +2 -2
  35. data/lib/prawn/svg/font.rb +3 -3
  36. data/lib/prawn/svg/interface.rb +12 -5
  37. data/lib/prawn/svg/url_loader.rb +1 -1
  38. data/lib/prawn/svg/version.rb +2 -2
  39. data/prawn-svg.gemspec +3 -3
  40. data/spec/integration_spec.rb +59 -2
  41. data/spec/prawn/svg/attributes/font_spec.rb +49 -0
  42. data/spec/prawn/svg/attributes/transform_spec.rb +56 -0
  43. data/spec/prawn/svg/calculators/aspect_ratio_spec.rb +2 -2
  44. data/spec/prawn/svg/calculators/document_sizing_spec.rb +3 -3
  45. data/spec/prawn/svg/color_spec.rb +36 -15
  46. data/spec/prawn/svg/document_spec.rb +4 -4
  47. data/spec/prawn/svg/elements/base_spec.rb +125 -0
  48. data/spec/prawn/svg/elements/gradient_spec.rb +61 -0
  49. data/spec/prawn/svg/elements/path_spec.rb +123 -0
  50. data/spec/prawn/svg/elements/style_spec.rb +23 -0
  51. data/spec/prawn/svg/{parser → elements}/text_spec.rb +7 -8
  52. data/spec/prawn/svg/font_spec.rb +12 -12
  53. data/spec/prawn/svg/interface_spec.rb +7 -7
  54. data/spec/prawn/svg/url_loader_spec.rb +2 -2
  55. data/spec/sample_svg/gradients.svg +40 -0
  56. data/spec/sample_svg/rect02.svg +8 -11
  57. data/spec/spec_helper.rb +1 -1
  58. metadata +46 -18
  59. data/lib/prawn/svg/element.rb +0 -304
  60. data/lib/prawn/svg/parser.rb +0 -268
  61. data/lib/prawn/svg/parser/image.rb +0 -81
  62. data/lib/prawn/svg/parser/path.rb +0 -392
  63. data/lib/prawn/svg/parser/text.rb +0 -80
  64. data/spec/prawn/svg/element_spec.rb +0 -127
  65. data/spec/prawn/svg/parser/path_spec.rb +0 -89
  66. data/spec/prawn/svg/parser_spec.rb +0 -55
@@ -0,0 +1,33 @@
1
+ class Prawn::SVG::Elements::Rect < Prawn::SVG::Elements::Base
2
+ def parse
3
+ require_attributes 'width', 'height'
4
+
5
+ @x = x(attributes['x'] || '0')
6
+ @y = y(attributes['y'] || '0')
7
+ @width = distance(attributes['width'], :x)
8
+ @height = distance(attributes['height'], :y)
9
+
10
+ require_positive_value @width, @height
11
+
12
+ @radius = distance(attributes['rx'] || attributes['ry'])
13
+ if @radius
14
+ # If you implement separate rx and ry in the future, you'll want to change this
15
+ # so that rx is constrained to @width/2 and ry is constrained to @height/2.
16
+ max_value = [@width, @height].min / 2.0
17
+ @radius = clamp(@radius, 0, max_value)
18
+ end
19
+ end
20
+
21
+ def apply
22
+ if @radius
23
+ # n.b. does not support both rx and ry being specified with different values
24
+ add_call "rounded_rectangle", [@x, @y], @width, @height, @radius
25
+ else
26
+ add_call "rectangle", [@x, @y], @width, @height
27
+ end
28
+ end
29
+
30
+ def bounding_box
31
+ [@x, @y, @x + @width, @y - @height]
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ class Prawn::SVG::Elements::Root < Prawn::SVG::Elements::Base
2
+ def apply
3
+ add_call 'fill_color', '000000'
4
+ add_call 'transformation_matrix', @document.sizing.x_scale, 0, 0, @document.sizing.y_scale, 0, 0
5
+ add_call 'transformation_matrix', 1, 0, 0, 1, @document.sizing.x_offset, @document.sizing.y_offset
6
+
7
+ process_child_elements
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ class Prawn::SVG::Elements::Style < Prawn::SVG::Elements::Base
2
+ def parse
3
+ if @document.css_parser
4
+ data = source.texts.map(&:value).join
5
+ @document.css_parser.add_block!(data)
6
+ end
7
+
8
+ raise SkipElementQuietly
9
+ end
10
+ end
@@ -0,0 +1,87 @@
1
+ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
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
11
+
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
17
+
18
+ @x_positions ||= state[:text_x_positions] || [document.x(0)]
19
+ @y_positions ||= state[:text_y_positions] || [document.y(0)]
20
+ end
21
+
22
+ def apply
23
+ raise SkipElementQuietly if state[:display] == "none"
24
+
25
+ add_call_and_enter "text_group" if name == 'text'
26
+
27
+ if attributes['dx'] || attributes['dy']
28
+ add_call_and_enter "translate", document.distance(attributes['dx'] || 0), -document.distance(attributes['dy'] || 0)
29
+ end
30
+
31
+ opts = {}
32
+ if size = state[:font_size]
33
+ opts[:size] = size
34
+ end
35
+ opts[:style] = state[:font_subfamily]
36
+
37
+ # This is not a prawn option but we can't work out how to render it here -
38
+ # it's handled by SVG#rewrite_call_arguments
39
+ if (anchor = attributes['text-anchor'] || state[:text_anchor]) &&
40
+ ['start', 'middle', 'end'].include?(anchor)
41
+ opts[:text_anchor] = anchor
42
+ end
43
+
44
+ if spacing = attributes['letter-spacing']
45
+ add_call_and_enter 'character_spacing', document.points(spacing)
46
+ end
47
+
48
+ source.children.each do |child|
49
+ if child.node_type == :text
50
+ text = child.value.strip.gsub(state[:preserve_space] ? /[\n\t]/ : /\s+/, " ")
51
+
52
+ while text != ""
53
+ opts[:at] = [@x_positions.first, @y_positions.first]
54
+
55
+ if @x_positions.length > 1 || @y_positions.length > 1
56
+ # TODO : isn't this just text.shift ?
57
+ add_call 'draw_text', text[0..0], opts.dup
58
+ text = text[1..-1]
59
+
60
+ @x_positions.shift if @x_positions.length > 1
61
+ @y_positions.shift if @y_positions.length > 1
62
+ else
63
+ add_call @relative ? 'relative_draw_text' : 'draw_text', text, opts.dup
64
+ @relative = true
65
+ break
66
+ end
67
+ end
68
+
69
+ elsif child.name == "tspan"
70
+ add_call 'save'
71
+
72
+ new_state = state.dup
73
+ new_state[:text_x_positions] = @x_positions
74
+ new_state[:text_y_positions] = @y_positions
75
+ new_state[:text_relative] = @relative
76
+ new_state[:text_anchor] = opts[:text_anchor]
77
+
78
+ Prawn::SVG::Elements::Text.new(document, child, calls, new_state).process
79
+
80
+ add_call 'restore'
81
+
82
+ else
83
+ warnings << "Unknown tag '#{child.name}' inside text tag; ignoring"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,29 @@
1
+ class Prawn::SVG::Elements::Use < Prawn::SVG::Elements::Base
2
+ def parse
3
+ require_attributes 'xlink:href'
4
+
5
+ href = attributes['xlink:href']
6
+
7
+ if href[0..0] != '#'
8
+ raise SkipElementError, "use tag has an href that is not a reference to an id; this is not supported"
9
+ end
10
+
11
+ id = href[1..-1]
12
+ @definition_element = @document.elements_by_id[id]
13
+
14
+ if @definition_element.nil?
15
+ raise SkipElementError, "no tag with ID '#{id}' was found, referenced by use tag"
16
+ end
17
+
18
+ @x = attributes['x']
19
+ @y = attributes['y']
20
+ end
21
+
22
+ def apply
23
+ if @x || @y
24
+ add_call_and_enter "translate", distance(@x || 0, :x), -distance(@y || 0, :y)
25
+ end
26
+
27
+ add_calls_from_element @definition_element
28
+ end
29
+ end
@@ -1,5 +1,5 @@
1
1
  module Prawn
2
- module Svg
2
+ module SVG
3
3
  module Extension
4
4
  #
5
5
  # Draws an SVG document into the PDF.
@@ -15,7 +15,7 @@ module Prawn
15
15
  # svg IO.read("example.svg"), :at => [100, 300], :width => 600
16
16
  #
17
17
  def svg(data, options = {}, &block)
18
- svg = Prawn::Svg::Interface.new(data, self, options, &block)
18
+ svg = Prawn::SVG::Interface.new(data, self, options, &block)
19
19
  svg.draw
20
20
  {:warnings => svg.document.warnings, :width => svg.document.sizing.output_width, :height => svg.document.sizing.output_height}
21
21
  end
@@ -1,4 +1,4 @@
1
- class Prawn::Svg::Font
1
+ class Prawn::SVG::Font
2
2
  GENERIC_CSS_FONT_MAPPING = {
3
3
  "serif" => "Times-Roman",
4
4
  "sans-serif" => "Helvetica",
@@ -34,12 +34,12 @@ class Prawn::Svg::Font
34
34
  # This method is passed prawn's font_families hash. It'll be pre-populated with the fonts that prawn natively
35
35
  # supports. We'll add fonts we find in the font path to this hash.
36
36
  def self.load_external_fonts(fonts)
37
- Prawn::Svg::Interface.font_path.uniq.collect {|path| Dir["#{path}/**/*"]}.flatten.each do |filename|
37
+ Prawn::SVG::Interface.font_path.uniq.collect {|path| Dir["#{path}/**/*"]}.flatten.each do |filename|
38
38
  information = font_information(filename) rescue nil
39
39
  if information && font_name = (information[16] || information[1])
40
40
  subfamily = (information[17] || information[2] || "normal").gsub(/\s+/, "_").downcase.to_sym
41
41
  subfamily = :normal if subfamily == :regular
42
- (fonts[font_name] ||= {})[subfamily] = filename
42
+ (fonts[font_name] ||= {})[subfamily] ||= filename
43
43
  end
44
44
  end
45
45
 
@@ -1,9 +1,9 @@
1
1
  #
2
- # Prawn::Svg::Interface makes a Prawn::Svg::Document instance, uses that object to parse the supplied
2
+ # Prawn::SVG::Interface makes a Prawn::SVG::Document instance, uses that object to parse the supplied
3
3
  # SVG into Prawn-compatible method calls, and then calls the Prawn methods.
4
4
  #
5
5
  module Prawn
6
- module Svg
6
+ module SVG
7
7
  class Interface
8
8
  VALID_OPTIONS = [:at, :position, :vposition, :width, :height, :cache_images, :fallback_font_name]
9
9
 
@@ -17,7 +17,7 @@ module Prawn
17
17
  attr_reader :data, :prawn, :document, :options
18
18
 
19
19
  #
20
- # Creates a Prawn::Svg object.
20
+ # Creates a Prawn::SVG object.
21
21
  #
22
22
  # +data+ is the SVG data to convert. +prawn+ is your Prawn::Document object.
23
23
  #
@@ -40,7 +40,7 @@ module Prawn
40
40
  @prawn = prawn
41
41
  @options = options
42
42
 
43
- Prawn::Svg::Font.load_external_fonts(prawn.font_families)
43
+ Prawn::SVG::Font.load_external_fonts(prawn.font_families)
44
44
 
45
45
  @document = Document.new(data, [prawn.bounds.width, prawn.bounds.height], options, &block)
46
46
  end
@@ -54,10 +54,17 @@ module Prawn
54
54
  return
55
55
  end
56
56
 
57
+ @document.warnings.clear
58
+
57
59
  prawn.bounding_box(position, :width => @document.sizing.output_width, :height => @document.sizing.output_height) do
58
60
  prawn.save_graphics_state do
59
61
  clip_rectangle 0, 0, @document.sizing.output_width, @document.sizing.output_height
60
- proc_creator(prawn, Parser.new(@document).parse).call
62
+
63
+ calls = []
64
+ root_element = Prawn::SVG::Elements::Root.new(@document, @document.root, calls, fill: true)
65
+ root_element.process
66
+
67
+ proc_creator(prawn, calls).call
61
68
  end
62
69
  end
63
70
  end
@@ -1,7 +1,7 @@
1
1
  require 'open-uri'
2
2
  require 'base64'
3
3
 
4
- class Prawn::Svg::UrlLoader
4
+ class Prawn::SVG::UrlLoader
5
5
  attr_accessor :enable_cache, :enable_web
6
6
  attr_reader :url_cache
7
7
 
@@ -1,5 +1,5 @@
1
1
  module Prawn
2
- module Svg
3
- VERSION = '0.21.0'
2
+ module SVG
3
+ VERSION = '0.22.1'
4
4
  end
5
5
  end
data/prawn-svg.gemspec CHANGED
@@ -3,7 +3,7 @@ require File.expand_path('../lib/prawn/svg/version', __FILE__)
3
3
 
4
4
  spec = Gem::Specification.new do |gem|
5
5
  gem.name = 'prawn-svg'
6
- gem.version = Prawn::Svg::VERSION
6
+ gem.version = Prawn::SVG::VERSION
7
7
  gem.summary = "SVG renderer for Prawn PDF library"
8
8
  gem.description = "This gem allows you to render SVG directly into a PDF using the 'prawn' gem. Since PDF is vector-based, you'll get nice scaled graphics if you use SVG instead of an image."
9
9
  gem.has_rdoc = false
@@ -18,9 +18,9 @@ spec = Gem::Specification.new do |gem|
18
18
  gem.name = "prawn-svg"
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.required_ruby_version = '>= 1.9.3'
21
+ gem.required_ruby_version = '>= 2.0.0'
22
22
 
23
- gem.add_runtime_dependency "prawn", ">= 0.8.4", "< 3"
23
+ gem.add_runtime_dependency "prawn", ">= 0.11.1", "< 3"
24
24
  gem.add_runtime_dependency "css_parser", "~> 1.3"
25
25
  gem.add_development_dependency "rspec", "~> 3.0"
26
26
  gem.add_development_dependency "rake", "~> 10.1"
@@ -1,8 +1,65 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Prawn::Svg::Interface do
3
+ describe "Integration test" do
4
4
  root = "#{File.dirname(__FILE__)}/.."
5
5
 
6
+ describe "a basic SVG file" do
7
+ let(:document) { Prawn::SVG::Document.new(svg, [800, 600], {}) }
8
+ let(:element) { Prawn::SVG::Elements::Root.new(document, document.root, [], {}) }
9
+
10
+ let(:svg) do
11
+ <<-SVG
12
+ <svg width="100" height="200">
13
+ <style><![CDATA[
14
+ #puppy { fill: red; }
15
+ .animal { fill: green; }
16
+ rect { fill: blue; }
17
+ ]]></style>
18
+
19
+ <rect x="0" y="0" width="10" height="10"/>
20
+ <rect x="10" y="0" width="10" height="10" class="animal"/>
21
+ <rect x="20" y="0" width="10" height="10" class="animal" id="puppy"/>
22
+ <rect x="30" y="0" width="10" height="10" class="animal" id="puppy" style="fill: yellow;"/>
23
+ </svg>
24
+ SVG
25
+ end
26
+
27
+ it "is correctly converted to a call stack" do
28
+ element.process
29
+
30
+ expect(element.calls).to eq [
31
+ ["fill_color", ["000000"], []],
32
+ ["transformation_matrix", [1, 0, 0, 1, 0, 0], []],
33
+ ["transformation_matrix", [1, 0, 0, 1, 0, 0], []],
34
+ ["save", [], []], ["restore", [], []],
35
+ ["save", [], []],
36
+ ["fill_color", ["0000ff"], []],
37
+ ["fill", [], [
38
+ ["rectangle", [[0.0, 200.0], 10.0, 10.0], []]
39
+ ]],
40
+ ["restore", [], []],
41
+ ["save", [], []],
42
+ ["fill_color", ["008000"], []],
43
+ ["fill", [], [
44
+ ["rectangle", [[10.0, 200.0], 10.0, 10.0], []]
45
+ ]],
46
+ ["restore", [], []],
47
+ ["save", [], []],
48
+ ["fill_color", ["ff0000"], []],
49
+ ["fill", [], [
50
+ ["rectangle", [[20.0, 200.0], 10.0, 10.0], []]
51
+ ]],
52
+ ["restore", [], []],
53
+ ["save", [], []],
54
+ ["fill_color", ["ffff00"], []],
55
+ ["fill", [], [
56
+ ["rectangle", [[30.0, 200.0], 10.0, 10.0], []]
57
+ ]],
58
+ ["restore", [], []]
59
+ ]
60
+ end
61
+ end
62
+
6
63
  context "with option :position" do
7
64
  let(:svg) { IO.read("#{root}/spec/sample_svg/cubic01a.svg") }
8
65
 
@@ -51,7 +108,7 @@ describe Prawn::Svg::Interface do
51
108
  doc.url_loader.url_cache["https://raw.githubusercontent.com/mogest/prawn-svg/master/spec/sample_images/mushroom-long.jpg"] = IO.read("#{root}/spec/sample_images/mushroom-long.jpg")
52
109
  end
53
110
 
54
- warnings = r[:warnings].reject {|w| w =~ /Verdana/ && w =~ /is not a known font/ }
111
+ warnings = r[:warnings].reject {|w| w =~ /Verdana/ && w =~ /is not a known font/ || w =~ /render gradients$/}
55
112
  end
56
113
  warnings.should == []
57
114
  end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe Prawn::SVG::Attributes::Font do
4
+ class FontTestElement
5
+ include Prawn::SVG::Attributes::Font
6
+
7
+ attr_accessor :attributes, :warnings
8
+
9
+ def initialize
10
+ @state = {}
11
+ @warnings = []
12
+ end
13
+ end
14
+
15
+ let(:element) { FontTestElement.new }
16
+ let(:document) { double(fallback_font_name: "Times-Roman") }
17
+
18
+ before do
19
+ allow(element).to receive(:document).and_return(document)
20
+ element.attributes = {"font-family" => family}
21
+ end
22
+
23
+ describe "#parse_font_attributes_and_call" do
24
+ context "with a specified existing font" do
25
+ let(:family) { "Helvetica" }
26
+
27
+ it "uses a font if it can find it" do
28
+ expect(element).to receive(:add_call_and_enter).with("font", "Helvetica", style: :normal)
29
+ element.parse_font_attributes_and_call
30
+ end
31
+ end
32
+
33
+ context "with a specified non-existing font" do
34
+ let(:family) { "Font That Doesn't Exist" }
35
+
36
+ it "uses the fallback font if specified" do
37
+ expect(element).to receive(:add_call_and_enter).with("font", "Times-Roman", style: :normal)
38
+ element.parse_font_attributes_and_call
39
+ end
40
+
41
+ it "doesn't call the font method if there's no fallback font" do
42
+ allow(document).to receive(:fallback_font_name).and_return(nil)
43
+ expect(element).to_not receive(:add_call_and_enter)
44
+ element.parse_font_attributes_and_call
45
+ expect(element.warnings.length).to eq 1
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe Prawn::SVG::Attributes::Transform do
4
+ class TransformTestElement
5
+ include Prawn::SVG::Attributes::Transform
6
+
7
+ attr_accessor :attributes, :warnings
8
+
9
+ def initialize
10
+ @warnings = []
11
+ @attributes = {}
12
+ end
13
+ end
14
+
15
+ let(:element) { TransformTestElement.new }
16
+
17
+ describe "#parse_transform_attribute_and_call" do
18
+ subject { element.send :parse_transform_attribute_and_call }
19
+
20
+ describe "translate" do
21
+ it "handles a missing y argument" do
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)
25
+
26
+ element.attributes['transform'] = 'translate(-5.5)'
27
+ subject
28
+ end
29
+ end
30
+
31
+ describe "rotate" do
32
+ it "handles a single angle argument" do
33
+ expect(element).to receive(:add_call_and_enter).with('rotate', -5.5, :origin => [0, 0])
34
+ expect(element).to receive(:y).with('0').and_return(0)
35
+
36
+ element.attributes['transform'] = 'rotate(5.5)'
37
+ subject
38
+ end
39
+
40
+ it "handles three arguments" do
41
+ expect(element).to receive(:add_call_and_enter).with('rotate', -5.5, :origin => [1.0, 2.0])
42
+ expect(element).to receive(:x).with(1.0).and_return(1.0)
43
+ expect(element).to receive(:y).with(2.0).and_return(2.0)
44
+
45
+ element.attributes['transform'] = 'rotate(5.5 1 2)'
46
+ subject
47
+ end
48
+
49
+ it "does nothing and warns if two arguments" do
50
+ expect(element).to receive(:warnings).and_return([])
51
+ element.attributes['transform'] = 'rotate(5.5 1)'
52
+ subject
53
+ end
54
+ end
55
+ end
56
+ end