prawn-svg 0.29.1 → 0.32.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +18 -0
  3. data/LICENSE +1 -1
  4. data/README.md +10 -9
  5. data/lib/prawn/svg/attributes/opacity.rb +4 -4
  6. data/lib/prawn/svg/attributes/transform.rb +2 -44
  7. data/lib/prawn/svg/elements/base.rb +11 -5
  8. data/lib/prawn/svg/elements/call_duplicator.rb +1 -1
  9. data/lib/prawn/svg/elements/gradient.rb +83 -25
  10. data/lib/prawn/svg/elements/image.rb +2 -2
  11. data/lib/prawn/svg/elements/path.rb +42 -29
  12. data/lib/prawn/svg/elements/root.rb +4 -1
  13. data/lib/prawn/svg/elements/text_component.rb +21 -5
  14. data/lib/prawn/svg/elements/use.rb +23 -7
  15. data/lib/prawn/svg/elements.rb +2 -0
  16. data/lib/prawn/svg/extensions/additional_gradient_transforms.rb +23 -0
  17. data/lib/prawn/svg/interface.rb +38 -8
  18. data/lib/prawn/svg/loaders/data.rb +1 -1
  19. data/lib/prawn/svg/loaders/file.rb +3 -1
  20. data/lib/prawn/svg/properties.rb +1 -0
  21. data/lib/prawn/svg/transform_parser.rb +72 -0
  22. data/lib/prawn/svg/version.rb +1 -1
  23. data/lib/prawn-svg.rb +4 -0
  24. data/prawn-svg.gemspec +3 -2
  25. data/spec/integration_spec.rb +24 -24
  26. data/spec/prawn/svg/attributes/opacity_spec.rb +85 -0
  27. data/spec/prawn/svg/attributes/transform_spec.rb +30 -35
  28. data/spec/prawn/svg/css/stylesheets_spec.rb +1 -19
  29. data/spec/prawn/svg/elements/base_spec.rb +16 -16
  30. data/spec/prawn/svg/elements/gradient_spec.rb +79 -4
  31. data/spec/prawn/svg/elements/line_spec.rb +12 -12
  32. data/spec/prawn/svg/elements/marker_spec.rb +27 -27
  33. data/spec/prawn/svg/elements/path_spec.rb +29 -17
  34. data/spec/prawn/svg/elements/polygon_spec.rb +9 -9
  35. data/spec/prawn/svg/elements/polyline_spec.rb +7 -7
  36. data/spec/prawn/svg/elements/text_spec.rb +65 -50
  37. data/spec/prawn/svg/loaders/data_spec.rb +8 -0
  38. data/spec/prawn/svg/pathable_spec.rb +4 -4
  39. data/spec/prawn/svg/transform_parser_spec.rb +94 -0
  40. data/spec/sample_svg/double_opacity.svg +6 -0
  41. data/spec/sample_svg/gradient_transform.svg +19 -0
  42. data/spec/sample_svg/links.svg +18 -0
  43. data/spec/sample_svg/radgrad01-bounding.svg +26 -0
  44. data/spec/sample_svg/radgrad01.svg +26 -0
  45. data/spec/sample_svg/svg_fill.svg +5 -0
  46. data/spec/sample_svg/text-decoration.svg +4 -0
  47. data/spec/sample_svg/transform.svg +20 -0
  48. data/spec/sample_svg/use_disordered.svg +17 -0
  49. data/spec/spec_helper.rb +2 -2
  50. metadata +48 -10
  51. data/.travis.yml +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2dbf86f272347e8867b1e7262a49163c316d0160fc35802ceb1f7b0f8ed90e25
4
- data.tar.gz: 4244134c0616eaf4aa9ca8ef19bbde2137e589d4f3929dd30b55c4c3d5136c36
3
+ metadata.gz: d054ded3422dc4cd1cb870dc09549c0a82c1e2813e314565c7a3aec70d95d188
4
+ data.tar.gz: 123a00b97f78f284a8e11fad37fed42cbec06d678675ec15ff7dafbcfe6505d5
5
5
  SHA512:
6
- metadata.gz: 3d1874a6766112c3084355c21b517b7a1d413f0e9e88c806cb236d69edc9fb7e1c3e253f13746c18e5e9b57d1221341d5c309494429dbae4a377ef91b99c8a52
7
- data.tar.gz: bbd80743596ad9c2c25871c5c6542d2ddfb0c3026ac5d0efb61a894b501aabc6657e048f3d52b0b0e42e634e68b0d30f7e5096ce9bfdc81d4a7ffc7ac08d65e9
6
+ metadata.gz: 4ece284ced16e7b6f4d7562c17a97780375f4e255eb4199d489cfb1fa473dc1f65e6b9fa48894079f0756b42a749cc58f34c00507e19ccdfddab55723c82311d
7
+ data.tar.gz: 8826f6f457372cc7df38869e6448974f5731fdee1040bca44b8446b71c57e0713340481a3400705c8fd11ec5d13959dcd99f24a0fe8820c89a37ca725545656c
@@ -0,0 +1,18 @@
1
+ name: test
2
+ on: [push, pull_request]
3
+ jobs:
4
+ rake:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ fail-fast: false
8
+ matrix:
9
+ ruby: [2.3, 2.4, 2.5, 2.6, 2.7, '3.0']
10
+ steps:
11
+ - uses: actions/checkout@v2
12
+ - name: Set up Ruby
13
+ uses: ruby/setup-ruby@v1
14
+ with:
15
+ bundler-cache: true
16
+ ruby-version: ${{ matrix.ruby }}
17
+ - name: Run tests
18
+ run: bundle exec rake
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License
2
2
 
3
- Copyright 2010-2013 Roger Nesbitt
3
+ Copyright 2010-2019 Roger Nesbitt
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # prawn-svg
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/prawn-svg.svg)](https://badge.fury.io/rb/prawn-svg)
4
- [![Build Status](https://travis-ci.org/mogest/prawn-svg.svg?branch=master)](https://travis-ci.org/mogest/prawn-svg)
4
+ ![Build Status](https://github.com/mogest/prawn-svg/actions/workflows/test.yml/badge.svg?branch=master)
5
5
 
6
6
  An SVG renderer for the Prawn PDF library.
7
7
 
@@ -10,7 +10,7 @@ This will take an SVG document as input and render it into your PDF. Find out m
10
10
  http://github.com/prawnpdf/prawn
11
11
 
12
12
  prawn-svg is compatible with all versions of Prawn from 0.11.1 onwards, including the 1.x and 2.x series.
13
- The minimum Ruby version required is 2.1.0.
13
+ The minimum Ruby version required is 2.3.0.
14
14
 
15
15
  ## Using prawn-svg
16
16
 
@@ -60,8 +60,8 @@ 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
- - `<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`
63
+ - `<text>`, `<tspan>` and `<tref>` with attributes `x`, `y`, `dx`, `dy`, `rotate`, 'textLength', 'lengthAdjust', and with extra properties
64
+ `text-anchor`, `text-decoration` (underline only), `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
 
@@ -76,7 +76,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
76
76
 
77
77
  - `<marker>`
78
78
 
79
- - `<linearGradient>` is implemented with Prawn 2.2.0+ (gradientTransform, spreadMethod and stop-opacity are unimplemented.)
79
+ - `<linearGradient>` and `<radialGradient>` are implemented on Prawn 2.2.0+ with attributes `gradientUnits` and `gradientTransform` (spreadMethod and stop-opacity are unimplemented.)
80
80
 
81
81
  - `<switch>` and `<foreignObject>`, although prawn-svg cannot handle any data that is not SVG so `<foreignObject>`
82
82
  tags are always ignored.
@@ -91,7 +91,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
91
91
 
92
92
  - the <tt>preserveAspectRatio</tt> attribute on <tt>&lt;svg&gt;</tt>, <tt>&lt;image&gt;</tt> and `<marker>` elements
93
93
 
94
- - transform methods: <tt>translate()</tt>, <tt>rotate()</tt>, <tt>scale()</tt>, <tt>matrix()</tt>
94
+ - transform methods: `translate`, `translateX`, `translateY`, `rotate`, `scale`, `skewX`, `skewY`, `matrix`
95
95
 
96
96
  - colors: HTML standard names, <tt>#xxx</tt>, <tt>#xxxxxx</tt>, <tt>rgb(1, 2, 3)</tt>, <tt>rgb(1%, 2%, 3%)</tt>
97
97
 
@@ -116,7 +116,7 @@ implemented, but `!important` is not.
116
116
 
117
117
  ## Not supported
118
118
 
119
- prawn-svg does not support radial gradients, patterns or filters.
119
+ prawn-svg does not support hyperlinks, patterns or filters.
120
120
 
121
121
  It does not support text in the clip area, but you can clip shapes and text by any shape.
122
122
 
@@ -135,5 +135,6 @@ Mac OS X and Debian Linux users. You can add to the font path:
135
135
 
136
136
  In your Gemfile, put `gem 'prawn-svg'` before `gem 'prawn-rails'` so that prawn-rails can see the prawn-svg extension.
137
137
 
138
- --
139
- Copyright Roger Nesbitt <roger@seriousorange.com>. MIT licence.
138
+ ## Licence
139
+
140
+ MIT licence. Copyright Roger Nesbitt.
@@ -1,13 +1,13 @@
1
1
  module Prawn::SVG::Attributes::Opacity
2
2
  def parse_opacity_attributes_and_call
3
3
  # We can't do nested opacities quite like the SVG requires, but this is close enough.
4
- fill_opacity = stroke_opacity = clamp(properties.opacity.to_f, 0, 1) if properties.opacity
4
+ opacity = clamp(properties.opacity.to_f, 0, 1) if properties.opacity
5
5
  fill_opacity = clamp(properties.fill_opacity.to_f, 0, 1) if properties.fill_opacity
6
6
  stroke_opacity = clamp(properties.stroke_opacity.to_f, 0, 1) if properties.stroke_opacity
7
7
 
8
- if fill_opacity || stroke_opacity
9
- state.fill_opacity *= fill_opacity || 1
10
- state.stroke_opacity *= stroke_opacity || 1
8
+ if opacity || fill_opacity || stroke_opacity
9
+ state.fill_opacity *= [opacity || 1, fill_opacity || 1].min
10
+ state.stroke_opacity *= [opacity || 1, stroke_opacity || 1].min
11
11
 
12
12
  add_call_and_enter 'transparent', state.fill_opacity, state.stroke_opacity
13
13
  end
@@ -2,49 +2,7 @@ module Prawn::SVG::Attributes::Transform
2
2
  def parse_transform_attribute_and_call
3
3
  return unless transform = attributes['transform']
4
4
 
5
- parse_css_method_calls(transform).each do |name, arguments|
6
- case name
7
- when 'translate'
8
- x, y = arguments
9
- add_call_and_enter name, x_pixels(x.to_f), -y_pixels(y.to_f)
10
-
11
- when 'rotate'
12
- r, x, y = arguments.collect {|a| a.to_f}
13
- case arguments.length
14
- when 1
15
- add_call_and_enter name, -r, :origin => [0, y('0')]
16
- when 3
17
- add_call_and_enter name, -r, :origin => [x(x), y(y)]
18
- else
19
- warnings << "transform 'rotate' must have either one or three arguments"
20
- end
21
-
22
- when 'scale'
23
- x_scale = arguments[0].to_f
24
- y_scale = (arguments[1] || x_scale).to_f
25
- add_call_and_enter "transformation_matrix", x_scale, 0, 0, y_scale, 0, 0
26
-
27
- when 'matrix'
28
- if arguments.length != 6
29
- warnings << "transform 'matrix' must have six arguments"
30
- else
31
- a, b, c, d, e, f = arguments.collect {|argument| argument.to_f}
32
- add_call_and_enter "transformation_matrix", a, -b, -c, d, x_pixels(e), -y_pixels(f)
33
- end
34
-
35
- else
36
- warnings << "Unknown transformation '#{name}'; ignoring"
37
- end
38
- end
39
- end
40
-
41
- private
42
-
43
- def parse_css_method_calls(string)
44
- string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
45
- name, argument_string = call
46
- arguments = argument_string.strip.split(/\s*[,\s]\s*/)
47
- [name, arguments]
48
- end
5
+ matrix = parse_transform_attribute(transform)
6
+ add_call_and_enter "transformation_matrix", *matrix unless matrix == [1, 0, 0, 1, 0, 0]
49
7
  end
50
8
  end
@@ -11,6 +11,8 @@ class Prawn::SVG::Elements::Base
11
11
  include Prawn::SVG::Attributes::Stroke
12
12
  include Prawn::SVG::Attributes::Space
13
13
 
14
+ include Prawn::SVG::TransformParser
15
+
14
16
  PAINT_TYPES = %w(fill stroke)
15
17
  COMMA_WSP_REGEXP = Prawn::SVG::Elements::COMMA_WSP_REGEXP
16
18
  SVG_NAMESPACE = "http://www.w3.org/2000/svg"
@@ -89,12 +91,12 @@ class Prawn::SVG::Elements::Base
89
91
  parse_xml_space_attribute
90
92
  end
91
93
 
92
- def add_call(name, *arguments)
93
- @calls << [name.to_s, arguments, []]
94
+ def add_call(name, *arguments, **kwarguments)
95
+ @calls << [name.to_s, arguments, kwarguments, []]
94
96
  end
95
97
 
96
- def add_call_and_enter(name, *arguments)
97
- @calls << [name.to_s, arguments, []]
98
+ def add_call_and_enter(name, *arguments, **kwarguments)
99
+ @calls << [name.to_s, arguments, kwarguments, []]
98
100
  @calls = @calls.last.last
99
101
  end
100
102
 
@@ -166,7 +168,7 @@ class Prawn::SVG::Elements::Base
166
168
  command = stroke ? 'fill_and_stroke' : 'fill'
167
169
 
168
170
  if computed_properties.fill_rule == 'evenodd'
169
- add_call_and_enter(command, {fill_rule: :even_odd})
171
+ add_call_and_enter(command, fill_rule: :even_odd)
170
172
  else
171
173
  add_call_and_enter(command)
172
174
  end
@@ -265,4 +267,8 @@ class Prawn::SVG::Elements::Base
265
267
  element = document.elements_by_id[matches[1]] if matches
266
268
  element if element && (expected_type.nil? || element.name == expected_type)
267
269
  end
270
+
271
+ def href_attribute
272
+ attributes['xlink:href'] || attributes['href']
273
+ end
268
274
  end
@@ -12,7 +12,7 @@ module Prawn::SVG::Elements::CallDuplicator
12
12
  end
13
13
 
14
14
  def duplicate_call(call)
15
- [call[0], duplicate_array(call[1]), duplicate_calls(call[2])]
15
+ [call[0], duplicate_array(call[1]), duplicate_hash(call[2]), duplicate_calls(call[3])]
16
16
  end
17
17
 
18
18
  def duplicate_array(array)
@@ -1,10 +1,18 @@
1
1
  class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
2
- TAG_NAME_TO_TYPE = {"linearGradient" => :linear}
2
+ attr_reader :parent_gradient
3
+ attr_reader :x1, :y1, :x2, :y2, :cx, :cy, :fx, :fy, :radius, :units, :stops, :transform_matrix
4
+
5
+ TAG_NAME_TO_TYPE = {
6
+ "linearGradient" => :linear,
7
+ "radialGradient" => :radial
8
+ }
3
9
 
4
10
  def parse
5
11
  # A gradient tag without an ID is inaccessible and can never be used
6
12
  raise SkipElementQuietly if attributes['id'].nil?
7
13
 
14
+ @parent_gradient = document.gradients[href_attribute[1..-1]] if href_attribute && href_attribute[0] == '#'
15
+
8
16
  assert_compatible_prawn_version
9
17
  load_gradient_configuration
10
18
  load_coordinates
@@ -16,26 +24,56 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
16
24
  end
17
25
 
18
26
  def gradient_arguments(element)
19
- case @units
20
- when :bounding_box
21
- x1, y1, x2, y2 = element.bounding_box
22
- return if y2.nil?
27
+ # Passing in a transformation matrix to the apply_transformations option is supported
28
+ # by a monkey patch installed by prawn-svg. Prawn only sees this as a truthy variable.
29
+ #
30
+ # See Prawn::SVG::Extensions::AdditionalGradientTransforms for details.
31
+ base_arguments = {stops: stops, apply_transformations: transform_matrix || true}
32
+
33
+ arguments = specific_gradient_arguments(element)
34
+ arguments.merge(base_arguments) if arguments
35
+ end
23
36
 
24
- width = x2 - x1
25
- height = y1 - y2
37
+ private
26
38
 
27
- from = [x1 + width * @x1, y1 - height * @y1]
28
- to = [x1 + width * @x2, y1 - height * @y2]
39
+ def specific_gradient_arguments(element)
40
+ if units == :bounding_box
41
+ bounding_x1, bounding_y1, bounding_x2, bounding_y2 = element.bounding_box
42
+ return if bounding_y2.nil?
29
43
 
30
- when :user_space
31
- from = [@x1, @y1]
32
- to = [@x2, @y2]
44
+ width = bounding_x2 - bounding_x1
45
+ height = bounding_y1 - bounding_y2
33
46
  end
34
47
 
35
- {from: from, to: to, stops: @stops}
36
- end
48
+ case [type, units]
49
+ when [:linear, :bounding_box]
50
+ from = [bounding_x1 + width * x1, bounding_y1 - height * y1]
51
+ to = [bounding_x1 + width * x2, bounding_y1 - height * y2]
37
52
 
38
- private
53
+ {from: from, to: to}
54
+
55
+ when [:linear, :user_space]
56
+ {from: [x1, y1], to: [x2, y2]}
57
+
58
+ when [:radial, :bounding_box]
59
+ center = [bounding_x1 + width * cx, bounding_y1 - height * cy]
60
+ focus = [bounding_x1 + width * fx, bounding_y1 - height * fy]
61
+
62
+ # Note: Chrome, at least, implements radial bounding box radiuses as
63
+ # having separate X and Y components, so in bounding box mode their
64
+ # gradients come out as ovals instead of circles. PDF radial shading
65
+ # doesn't have the option to do this, and it's confusing why the
66
+ # Chrome user space gradients don't apply the same logic anyway.
67
+ hypot = Math.sqrt(width * width + height * height)
68
+ {from: focus, r1: 0, to: center, r2: radius * hypot}
69
+
70
+ when [:radial, :user_space]
71
+ {from: [fx, fy], r1: 0, to: [cx, cy], r2: radius}
72
+
73
+ else
74
+ raise "unexpected type/unit system"
75
+ end
76
+ end
39
77
 
40
78
  def type
41
79
  TAG_NAME_TO_TYPE.fetch(name)
@@ -51,10 +89,7 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
51
89
  @units = attributes["gradientUnits"] == 'userSpaceOnUse' ? :user_space : :bounding_box
52
90
 
53
91
  if transform = attributes["gradientTransform"]
54
- matrix = transform.split(COMMA_WSP_REGEXP).map(&:to_f)
55
- if matrix != [1, 0, 0, 1, 0, 0]
56
- raise SkipElementError, "prawn-svg does not yet support gradients with a non-identity gradientTransform attribute"
57
- end
92
+ @transform_matrix = parse_transform_attribute(transform)
58
93
  end
59
94
 
60
95
  if (spread_method = attributes['spreadMethod']) && spread_method != "pad"
@@ -63,18 +98,35 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
63
98
  end
64
99
 
65
100
  def load_coordinates
66
- case @units
67
- when :bounding_box
101
+ case [type, units]
102
+ when [:linear, :bounding_box]
68
103
  @x1 = parse_zero_to_one(attributes["x1"], 0)
69
104
  @y1 = parse_zero_to_one(attributes["y1"], 0)
70
105
  @x2 = parse_zero_to_one(attributes["x2"], 1)
71
106
  @y2 = parse_zero_to_one(attributes["y2"], 0)
72
107
 
73
- when :user_space
108
+ when [:linear, :user_space]
74
109
  @x1 = x(attributes["x1"])
75
110
  @y1 = y(attributes["y1"])
76
111
  @x2 = x(attributes["x2"])
77
112
  @y2 = y(attributes["y2"])
113
+
114
+ when [:radial, :bounding_box]
115
+ @cx = parse_zero_to_one(attributes["cx"], 0.5)
116
+ @cy = parse_zero_to_one(attributes["cy"], 0.5)
117
+ @fx = parse_zero_to_one(attributes["fx"], cx)
118
+ @fy = parse_zero_to_one(attributes["fy"], cy)
119
+ @radius = parse_zero_to_one(attributes["r"], 0.5)
120
+
121
+ when [:radial, :user_space]
122
+ @cx = x(attributes["cx"] || '50%')
123
+ @cy = y(attributes["cy"] || '50%')
124
+ @fx = x(attributes["fx"] || attributes["cx"])
125
+ @fy = y(attributes["fy"] || attributes["cy"])
126
+ @radius = pixels(attributes["r"] || '50%')
127
+
128
+ else
129
+ raise "unexpected type/unit system"
78
130
  end
79
131
  end
80
132
 
@@ -100,10 +152,16 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
100
152
  end
101
153
  end
102
154
 
103
- raise SkipElementError, "gradient does not have any valid stops" if @stops.empty?
155
+ if stops.empty?
156
+ if parent_gradient.nil? || parent_gradient.stops.empty?
157
+ raise SkipElementError, "gradient does not have any valid stops"
158
+ end
104
159
 
105
- @stops.unshift([0, @stops.first.last]) if @stops.first.first > 0
106
- @stops.push([1, @stops.last.last]) if @stops.last.first < 1
160
+ @stops = parent_gradient.stops
161
+ else
162
+ stops.unshift([0, stops.first.last]) if stops.first.first > 0
163
+ stops.push([1, stops.last.last]) if stops.last.first < 1
164
+ end
107
165
  end
108
166
 
109
167
  def parse_zero_to_one(string, default = 0)
@@ -15,9 +15,9 @@ class Prawn::SVG::Elements::Image < Prawn::SVG::Elements::Base
15
15
 
16
16
  raise SkipElementQuietly if state.computed_properties.display == "none"
17
17
 
18
- @url = attributes['xlink:href'] || attributes['href']
18
+ @url = href_attribute
19
19
  if @url.nil?
20
- raise SkipElementError, "image tag must have an xlink:href"
20
+ raise SkipElementError, "image tag must have an href or xlink:href"
21
21
  end
22
22
 
23
23
  x = x(attributes['x'] || 0)
@@ -4,10 +4,31 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
4
4
 
5
5
  INSIDE_SPACE_REGEXP = /[ \t\r\n,]*/
6
6
  OUTSIDE_SPACE_REGEXP = /[ \t\r\n]*/
7
- INSIDE_REGEXP = /#{INSIDE_SPACE_REGEXP}([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:(?<=[0-9])[eE][+-]?[0-9]+)?)/
8
- VALUES_REGEXP = /^#{INSIDE_REGEXP}/
7
+ INSIDE_REGEXP = /#{INSIDE_SPACE_REGEXP}(?>([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:(?<=[0-9])[eE][+-]?[0-9]+)?))/
8
+ FLAG_REGEXP = /#{INSIDE_SPACE_REGEXP}([01])/
9
9
  COMMAND_REGEXP = /^#{OUTSIDE_SPACE_REGEXP}([A-Za-z])((?:#{INSIDE_REGEXP})*)#{OUTSIDE_SPACE_REGEXP}/
10
10
 
11
+ A_PARAMETERS_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{FLAG_REGEXP}#{FLAG_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}/
12
+ ONE_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}/
13
+ TWO_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}/
14
+ FOUR_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}/
15
+ SIX_PARAMETER_REGEXP = /^#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}#{INSIDE_REGEXP}/
16
+
17
+ COMMAND_MATCH_MAP = {
18
+ 'A' => A_PARAMETERS_REGEXP,
19
+ 'C' => SIX_PARAMETER_REGEXP,
20
+ 'H' => ONE_PARAMETER_REGEXP,
21
+ 'L' => TWO_PARAMETER_REGEXP,
22
+ 'M' => TWO_PARAMETER_REGEXP,
23
+ 'Q' => FOUR_PARAMETER_REGEXP,
24
+ 'S' => FOUR_PARAMETER_REGEXP,
25
+ 'T' => TWO_PARAMETER_REGEXP,
26
+ 'V' => ONE_PARAMETER_REGEXP,
27
+ 'Z' => //,
28
+ }
29
+
30
+ PARAMETERLESS_COMMANDS = COMMAND_MATCH_MAP.select { |_, v| v == // }.map(&:first)
31
+
11
32
  FLOAT_ERROR_DELTA = 1e-10
12
33
 
13
34
  attr_reader :commands
@@ -16,20 +37,20 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
16
37
  require_attributes 'd'
17
38
 
18
39
  @commands = []
40
+ @last_point = nil
19
41
 
20
42
  data = attributes["d"].gsub(/#{OUTSIDE_SPACE_REGEXP}$/, '')
21
43
 
22
44
  matched_commands = match_all(data, COMMAND_REGEXP)
23
45
  raise SkipElementError, "Invalid/unsupported syntax for SVG path data" if matched_commands.nil?
24
46
 
25
- catch :invalid_command do
26
- matched_commands.each do |matched_command|
27
- command = matched_command[1]
28
- matched_values = match_all(matched_command[2], VALUES_REGEXP)
29
- raise "should be impossible to have invalid inside data, but we ended up here" if matched_values.nil?
30
- values = matched_values.collect {|value| value[1].to_f}
31
- parse_path_command(command, values)
32
- end
47
+ matched_commands.each do |(command, parameters)|
48
+ regexp = COMMAND_MATCH_MAP[command.upcase] or break
49
+ matched_values = match_all(parameters, regexp) or break
50
+ values = matched_values.map { |value| value.map(&:to_f) }
51
+ break if values.empty? && !PARAMETERLESS_COMMANDS.include?(command.upcase)
52
+
53
+ parse_path_command(command, values)
33
54
  end
34
55
  end
35
56
 
@@ -48,8 +69,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
48
69
 
49
70
  case upcase_command
50
71
  when 'M' # moveto
51
- x = values.shift or throw :invalid_command
52
- y = values.shift or throw :invalid_command
72
+ x, y = values.shift
53
73
 
54
74
  if relative && @last_point
55
75
  x += @last_point.first
@@ -68,8 +88,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
68
88
 
69
89
  when 'L' # lineto
70
90
  while values.any?
71
- x = values.shift
72
- y = values.shift or throw :invalid_command
91
+ x, y = values.shift
73
92
  if relative && @last_point
74
93
  x += @last_point.first
75
94
  y += @last_point.last
@@ -80,22 +99,21 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
80
99
 
81
100
  when 'H' # horizontal lineto
82
101
  while values.any?
83
- x = values.shift
102
+ x = values.shift.first
84
103
  x += @last_point.first if relative && @last_point
85
104
  push_command Prawn::SVG::Pathable::Line.new([x, @last_point.last])
86
105
  end
87
106
 
88
107
  when 'V' # vertical lineto
89
108
  while values.any?
90
- y = values.shift
109
+ y = values.shift.first
91
110
  y += @last_point.last if relative && @last_point
92
111
  push_command Prawn::SVG::Pathable::Line.new([@last_point.first, y])
93
112
  end
94
113
 
95
114
  when 'C' # curveto
96
115
  while values.any?
97
- x1, y1, x2, y2, x, y = values.shift(6)
98
- throw :invalid_command unless y
116
+ x1, y1, x2, y2, x, y = values.shift
99
117
 
100
118
  if relative && @last_point
101
119
  x += @last_point.first
@@ -112,8 +130,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
112
130
 
113
131
  when 'S' # shorthand/smooth curveto
114
132
  while values.any?
115
- x2, y2, x, y = values.shift(4)
116
- throw :invalid_command unless y
133
+ x2, y2, x, y = values.shift
117
134
 
118
135
  if relative && @last_point
119
136
  x += @last_point.first
@@ -136,13 +153,11 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
136
153
  when 'Q', 'T' # quadratic curveto
137
154
  while values.any?
138
155
  if shorthand = upcase_command == 'T'
139
- x, y = values.shift(2)
156
+ x, y = values.shift
140
157
  else
141
- x1, y1, x, y = values.shift(4)
158
+ x1, y1, x, y = values.shift
142
159
  end
143
160
 
144
- throw :invalid_command unless y
145
-
146
161
  if relative && @last_point
147
162
  x += @last_point.first
148
163
  x1 += @last_point.first if x1
@@ -174,8 +189,7 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
174
189
  return unless @last_point
175
190
 
176
191
  while values.any?
177
- rx, ry, phi, fa, fs, x2, y2 = values.shift(7)
178
- throw :invalid_command unless y2
192
+ rx, ry, phi, fa, fs, x2, y2 = values.shift
179
193
 
180
194
  x1, y1 = @last_point
181
195
 
@@ -276,9 +290,8 @@ class Prawn::SVG::Elements::Path < Prawn::SVG::Elements::Base
276
290
  def match_all(string, regexp) # regexp must start with ^
277
291
  result = []
278
292
  while string != ""
279
- matches = string.match(regexp)
280
- result << matches
281
- return if matches.nil?
293
+ matches = string.match(regexp) or return
294
+ result << matches.captures
282
295
  string = matches.post_match
283
296
  end
284
297
  result
@@ -8,7 +8,10 @@ class Prawn::SVG::Elements::Root < Prawn::SVG::Elements::Base
8
8
  end
9
9
 
10
10
  def apply
11
- add_call 'fill_color', '000000'
11
+ if [nil, 'inherit', 'none', 'currentColor'].include?(properties.fill)
12
+ add_call 'fill_color', '000000'
13
+ end
14
+
12
15
  add_call 'transformation_matrix', @document.sizing.x_scale, 0, 0, @document.sizing.y_scale, 0, 0
13
16
  add_call 'transformation_matrix', 1, 0, 0, 1, -@document.sizing.x_offset, @document.sizing.y_offset
14
17
  end
@@ -2,7 +2,7 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
2
2
  attr_reader :commands
3
3
 
4
4
  Printable = Struct.new(:element, :text, :leading_space?, :trailing_space?)
5
- TextState = Struct.new(:parent, :x, :y, :dx, :dy, :rotation, :spacing, :mode)
5
+ TextState = Struct.new(:parent, :x, :y, :dx, :dy, :rotation, :spacing, :mode, :text_length, :length_adjust)
6
6
 
7
7
  def parse
8
8
  if state.inside_clip_path
@@ -14,6 +14,8 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
14
14
  state.text.dx = (attributes['dx'] || "").split(COMMA_WSP_REGEXP).collect { |n| x_pixels(n) }
15
15
  state.text.dy = (attributes['dy'] || "").split(COMMA_WSP_REGEXP).collect { |n| y_pixels(n) }
16
16
  state.text.rotation = (attributes['rotate'] || "").split(COMMA_WSP_REGEXP).collect(&:to_f)
17
+ state.text.text_length = normalize_length(attributes['textLength'])
18
+ state.text.length_adjust = attributes['lengthAdjust']
17
19
  state.text.spacing = calculate_character_spacing
18
20
  state.text.mode = calculate_text_rendering_mode
19
21
 
@@ -44,9 +46,11 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
44
46
  opts = {
45
47
  size: computed_properties.numerical_font_size,
46
48
  style: font && font.subfamily,
47
- text_anchor: computed_properties.text_anchor
49
+ text_anchor: computed_properties.text_anchor,
48
50
  }
49
51
 
52
+ opts[:decoration] = computed_properties.text_decoration unless computed_properties.text_decoration == 'none'
53
+
50
54
  if state.text.parent
51
55
  add_call_and_enter 'character_spacing', state.text.spacing unless state.text.spacing == state.text.parent.spacing
52
56
  add_call_and_enter 'text_rendering_mode', state.text.mode unless state.text.mode == state.text.parent.mode
@@ -133,11 +137,19 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
133
137
  opts.delete(:rotate)
134
138
  end
135
139
 
140
+ if state.text.text_length
141
+ if state.text.length_adjust == 'spacingAndGlyphs'
142
+ opts[:stretch_to_width] = state.text.text_length
143
+ else
144
+ opts[:pad_to_width] = state.text.text_length
145
+ end
146
+ end
147
+
136
148
  if remaining
137
- add_call 'draw_text', text[0..0], opts.dup
149
+ add_call 'draw_text', text[0..0], **opts.dup
138
150
  text = text[1..-1]
139
151
  else
140
- add_call 'draw_text', text, opts.dup
152
+ add_call 'draw_text', text, **opts.dup
141
153
 
142
154
  # we can get to this path with rotations still pending
143
155
  # solve this by shifting them out by the number of
@@ -173,7 +185,7 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
173
185
  end
174
186
 
175
187
  def find_referenced_element
176
- href = attributes['xlink:href']
188
+ href = href_attribute
177
189
 
178
190
  if href && href[0..0] == '#'
179
191
  element = document.elements_by_id[href[1..-1]]
@@ -222,4 +234,8 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
222
234
  # overridden, we don't want to call fill/stroke as draw_text does this for us
223
235
  def apply_drawing_call
224
236
  end
237
+
238
+ def normalize_length(length)
239
+ x_pixels(length) if length && length.match(/\d/)
240
+ end
225
241
  end