prawn-svg 0.29.1 → 0.32.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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +18 -0
- data/LICENSE +1 -1
- data/README.md +10 -9
- data/lib/prawn/svg/attributes/opacity.rb +4 -4
- data/lib/prawn/svg/attributes/transform.rb +2 -44
- data/lib/prawn/svg/elements/base.rb +11 -5
- data/lib/prawn/svg/elements/call_duplicator.rb +1 -1
- data/lib/prawn/svg/elements/gradient.rb +83 -25
- data/lib/prawn/svg/elements/image.rb +2 -2
- data/lib/prawn/svg/elements/path.rb +42 -29
- data/lib/prawn/svg/elements/root.rb +4 -1
- data/lib/prawn/svg/elements/text_component.rb +21 -5
- data/lib/prawn/svg/elements/use.rb +23 -7
- data/lib/prawn/svg/elements.rb +2 -0
- data/lib/prawn/svg/extensions/additional_gradient_transforms.rb +23 -0
- data/lib/prawn/svg/interface.rb +38 -8
- data/lib/prawn/svg/loaders/data.rb +1 -1
- data/lib/prawn/svg/loaders/file.rb +3 -1
- data/lib/prawn/svg/properties.rb +1 -0
- data/lib/prawn/svg/transform_parser.rb +72 -0
- data/lib/prawn/svg/version.rb +1 -1
- data/lib/prawn-svg.rb +4 -0
- data/prawn-svg.gemspec +3 -2
- data/spec/integration_spec.rb +24 -24
- data/spec/prawn/svg/attributes/opacity_spec.rb +85 -0
- data/spec/prawn/svg/attributes/transform_spec.rb +30 -35
- data/spec/prawn/svg/css/stylesheets_spec.rb +1 -19
- data/spec/prawn/svg/elements/base_spec.rb +16 -16
- data/spec/prawn/svg/elements/gradient_spec.rb +79 -4
- data/spec/prawn/svg/elements/line_spec.rb +12 -12
- data/spec/prawn/svg/elements/marker_spec.rb +27 -27
- data/spec/prawn/svg/elements/path_spec.rb +29 -17
- data/spec/prawn/svg/elements/polygon_spec.rb +9 -9
- data/spec/prawn/svg/elements/polyline_spec.rb +7 -7
- data/spec/prawn/svg/elements/text_spec.rb +65 -50
- data/spec/prawn/svg/loaders/data_spec.rb +8 -0
- data/spec/prawn/svg/pathable_spec.rb +4 -4
- data/spec/prawn/svg/transform_parser_spec.rb +94 -0
- data/spec/sample_svg/double_opacity.svg +6 -0
- data/spec/sample_svg/gradient_transform.svg +19 -0
- data/spec/sample_svg/links.svg +18 -0
- data/spec/sample_svg/radgrad01-bounding.svg +26 -0
- data/spec/sample_svg/radgrad01.svg +26 -0
- data/spec/sample_svg/svg_fill.svg +5 -0
- data/spec/sample_svg/text-decoration.svg +4 -0
- data/spec/sample_svg/transform.svg +20 -0
- data/spec/sample_svg/use_disordered.svg +17 -0
- data/spec/spec_helper.rb +2 -2
- metadata +48 -10
- data/.travis.yml +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d054ded3422dc4cd1cb870dc09549c0a82c1e2813e314565c7a3aec70d95d188
|
4
|
+
data.tar.gz: 123a00b97f78f284a8e11fad37fed42cbec06d678675ec15ff7dafbcfe6505d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# prawn-svg
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/prawn-svg)
|
4
|
-
|
4
|
+

|
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.
|
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><path></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><svg></tt>, <tt><g></tt> and <tt><symbol></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>`
|
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><svg></tt>, <tt><image></tt> and `<marker>` elements
|
93
93
|
|
94
|
-
- transform methods:
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
6
|
-
|
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,
|
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]),
|
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
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
height = y1 - y2
|
37
|
+
private
|
26
38
|
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
32
|
-
to = [@x2, @y2]
|
44
|
+
width = bounding_x2 - bounding_x1
|
45
|
+
height = bounding_y1 - bounding_y2
|
33
46
|
end
|
34
47
|
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
106
|
-
|
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 =
|
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
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
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
|
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
|
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
|
156
|
+
x, y = values.shift
|
140
157
|
else
|
141
|
-
x1, y1, x, y = values.shift
|
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
|
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
|
-
|
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 =
|
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
|