prawn-svg 0.21.0 → 0.22.1

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 (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