prawn-svg 0.35.1 → 0.36.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +3 -5
- data/README.md +4 -7
- data/lib/prawn/svg/attributes/opacity.rb +23 -8
- data/lib/prawn/svg/attributes/stroke.rb +7 -9
- data/lib/prawn/svg/attributes/transform.rb +1 -1
- data/lib/prawn/svg/calculators/pixels.rb +9 -22
- data/lib/prawn/svg/color.rb +58 -59
- data/lib/prawn/svg/css/font_parser.rb +46 -0
- data/lib/prawn/svg/document.rb +5 -1
- data/lib/prawn/svg/elements/base.rb +22 -30
- data/lib/prawn/svg/elements/gradient.rb +99 -74
- data/lib/prawn/svg/elements/line.rb +1 -1
- data/lib/prawn/svg/elements/marker.rb +2 -0
- data/lib/prawn/svg/elements/root.rb +1 -1
- data/lib/prawn/svg/elements/text_component.rb +3 -3
- data/lib/prawn/svg/funciri.rb +14 -0
- data/lib/prawn/svg/gradient_renderer.rb +313 -0
- data/lib/prawn/svg/length.rb +43 -0
- data/lib/prawn/svg/paint.rb +67 -0
- data/lib/prawn/svg/percentage.rb +24 -0
- data/lib/prawn/svg/properties.rb +208 -104
- data/lib/prawn/svg/renderer.rb +5 -0
- data/lib/prawn/svg/state.rb +5 -3
- data/lib/prawn/svg/transform_parser.rb +19 -13
- data/lib/prawn/svg/transform_utils.rb +37 -0
- data/lib/prawn/svg/version.rb +1 -1
- data/lib/prawn-svg.rb +7 -3
- data/prawn-svg.gemspec +4 -4
- data/spec/prawn/svg/attributes/opacity_spec.rb +27 -20
- data/spec/prawn/svg/attributes/transform_spec.rb +5 -2
- data/spec/prawn/svg/calculators/pixels_spec.rb +1 -2
- data/spec/prawn/svg/color_spec.rb +22 -52
- data/spec/prawn/svg/elements/base_spec.rb +9 -10
- data/spec/prawn/svg/elements/gradient_spec.rb +92 -36
- data/spec/prawn/svg/elements/marker_spec.rb +13 -15
- data/spec/prawn/svg/funciri_spec.rb +59 -0
- data/spec/prawn/svg/length_spec.rb +89 -0
- data/spec/prawn/svg/paint_spec.rb +96 -0
- data/spec/prawn/svg/pathable_spec.rb +3 -3
- data/spec/prawn/svg/pdfmatrix_spec.rb +60 -0
- data/spec/prawn/svg/percentage_spec.rb +60 -0
- data/spec/prawn/svg/properties_spec.rb +124 -107
- data/spec/prawn/svg/transform_parser_spec.rb +13 -13
- data/spec/sample_svg/gradient_stress_test.svg +115 -0
- metadata +17 -5
- data/lib/prawn/svg/extensions/additional_gradient_transforms.rb +0 -23
@@ -1,6 +1,6 @@
|
|
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, :
|
3
|
+
attr_reader :x1, :y1, :x2, :y2, :cx, :cy, :r, :fx, :fy, :fr, :units, :stops, :transform_matrix, :wrap
|
4
4
|
|
5
5
|
TAG_NAME_TO_TYPE = {
|
6
6
|
'linearGradient' => :linear,
|
@@ -12,6 +12,8 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
12
12
|
raise SkipElementQuietly if attributes['id'].nil?
|
13
13
|
|
14
14
|
@parent_gradient = document.gradients[href_attribute[1..]] if href_attribute && href_attribute[0] == '#'
|
15
|
+
@transform_matrix = Matrix.identity(3)
|
16
|
+
@wrap = :pad
|
15
17
|
|
16
18
|
assert_compatible_prawn_version
|
17
19
|
load_gradient_configuration
|
@@ -24,57 +26,58 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
24
26
|
end
|
25
27
|
|
26
28
|
def gradient_arguments(element)
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
bbox = element.bounding_box
|
30
|
+
|
31
|
+
if type == :radial
|
32
|
+
{
|
33
|
+
from: [fx, fy],
|
34
|
+
r1: fr,
|
35
|
+
to: [cx, cy],
|
36
|
+
r2: r,
|
37
|
+
stops: stops,
|
38
|
+
matrix: matrix_for_bounding_box(*bbox),
|
39
|
+
wrap: wrap,
|
40
|
+
bounding_box: bbox
|
41
|
+
}
|
42
|
+
else
|
43
|
+
{
|
44
|
+
from: [x1, y1],
|
45
|
+
to: [x2, y2],
|
46
|
+
stops: stops,
|
47
|
+
matrix: matrix_for_bounding_box(*bbox),
|
48
|
+
wrap: wrap,
|
49
|
+
bounding_box: bbox
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def derive_attribute(name)
|
55
|
+
attributes[name] || parent_gradient&.derive_attribute(name)
|
35
56
|
end
|
36
57
|
|
37
58
|
private
|
38
59
|
|
39
|
-
def
|
60
|
+
def matrix_for_bounding_box(bounding_x1, bounding_y1, bounding_x2, bounding_y2)
|
40
61
|
if units == :bounding_box
|
41
|
-
bounding_x1, bounding_y1, bounding_x2, bounding_y2 = element.bounding_box
|
42
|
-
return if bounding_y2.nil?
|
43
|
-
|
44
62
|
width = bounding_x2 - bounding_x1
|
45
63
|
height = bounding_y1 - bounding_y2
|
46
|
-
end
|
47
|
-
|
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)]
|
52
|
-
|
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
64
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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 }
|
65
|
+
bounding_box_to_user_space_matrix = Matrix[
|
66
|
+
[width, 0.0, bounding_x1],
|
67
|
+
[0.0, height, document.sizing.output_height - bounding_y1],
|
68
|
+
[0.0, 0.0, 1.0]
|
69
|
+
]
|
72
70
|
|
71
|
+
svg_to_pdf_matrix * bounding_box_to_user_space_matrix * transform_matrix
|
73
72
|
else
|
74
|
-
|
73
|
+
svg_to_pdf_matrix * transform_matrix
|
75
74
|
end
|
76
75
|
end
|
77
76
|
|
77
|
+
def svg_to_pdf_matrix
|
78
|
+
@svg_to_pdf_matrix ||= Matrix[[1.0, 0.0, 0.0], [0.0, -1.0, document.sizing.output_height], [0.0, 0.0, 1.0]]
|
79
|
+
end
|
80
|
+
|
78
81
|
def type
|
79
82
|
TAG_NAME_TO_TYPE.fetch(name)
|
80
83
|
end
|
@@ -86,44 +89,47 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
86
89
|
end
|
87
90
|
|
88
91
|
def load_gradient_configuration
|
89
|
-
@units =
|
92
|
+
@units = derive_attribute('gradientUnits') == 'userSpaceOnUse' ? :user_space : :bounding_box
|
90
93
|
|
91
|
-
if (transform =
|
92
|
-
@transform_matrix = parse_transform_attribute(transform)
|
94
|
+
if (transform = derive_attribute('gradientTransform'))
|
95
|
+
@transform_matrix = parse_transform_attribute(transform, space: :svg)
|
93
96
|
end
|
94
97
|
|
95
|
-
if (spread_method =
|
96
|
-
|
98
|
+
if (spread_method = derive_attribute('spreadMethod'))
|
99
|
+
spread_method = spread_method.to_sym
|
100
|
+
@wrap = [:pad, :reflect, :repeat].include?(spread_method) ? spread_method : :pad
|
97
101
|
end
|
98
102
|
end
|
99
103
|
|
100
104
|
def load_coordinates
|
101
105
|
case [type, units]
|
102
106
|
when [:linear, :bounding_box]
|
103
|
-
@x1 =
|
104
|
-
@y1 =
|
105
|
-
@x2 =
|
106
|
-
@y2 =
|
107
|
+
@x1 = percentage_or_proportion(derive_attribute('x1'), 0.0)
|
108
|
+
@y1 = percentage_or_proportion(derive_attribute('y1'), 0.0)
|
109
|
+
@x2 = percentage_or_proportion(derive_attribute('x2'), 1.0)
|
110
|
+
@y2 = percentage_or_proportion(derive_attribute('y2'), 0.0)
|
107
111
|
|
108
112
|
when [:linear, :user_space]
|
109
|
-
@x1 = x(
|
110
|
-
@y1 =
|
111
|
-
@x2 = x(
|
112
|
-
@y2 =
|
113
|
+
@x1 = x(derive_attribute('x1'))
|
114
|
+
@y1 = y_pixels(derive_attribute('y1'))
|
115
|
+
@x2 = x(derive_attribute('x2'))
|
116
|
+
@y2 = y_pixels(derive_attribute('y2'))
|
113
117
|
|
114
118
|
when [:radial, :bounding_box]
|
115
|
-
@cx =
|
116
|
-
@cy =
|
117
|
-
@
|
118
|
-
@
|
119
|
-
@
|
119
|
+
@cx = percentage_or_proportion(derive_attribute('cx'), 0.5)
|
120
|
+
@cy = percentage_or_proportion(derive_attribute('cy'), 0.5)
|
121
|
+
@r = percentage_or_proportion(derive_attribute('r'), 0.5)
|
122
|
+
@fx = percentage_or_proportion(derive_attribute('fx'), cx)
|
123
|
+
@fy = percentage_or_proportion(derive_attribute('fy'), cy)
|
124
|
+
@fr = percentage_or_proportion(derive_attribute('fr'), 0.0)
|
120
125
|
|
121
126
|
when [:radial, :user_space]
|
122
|
-
@cx = x(
|
123
|
-
@cy =
|
124
|
-
@
|
125
|
-
@
|
126
|
-
@
|
127
|
+
@cx = x(derive_attribute('cx') || '50%')
|
128
|
+
@cy = y_pixels(derive_attribute('cy') || '50%')
|
129
|
+
@r = pixels(derive_attribute('r') || '50%')
|
130
|
+
@fx = x(derive_attribute('fx') || derive_attribute('cx'))
|
131
|
+
@fy = y_pixels(derive_attribute('fy') || derive_attribute('cy'))
|
132
|
+
@fr = pixels(derive_attribute('fr') || '0%')
|
127
133
|
|
128
134
|
else
|
129
135
|
raise 'unexpected type/unit system'
|
@@ -139,14 +145,14 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
139
145
|
element.name == 'stop' && element.attributes['offset']
|
140
146
|
end
|
141
147
|
|
142
|
-
@stops = stop_elements.
|
143
|
-
offset =
|
148
|
+
@stops = stop_elements.each_with_object([]) do |child, result|
|
149
|
+
offset = percentage_or_proportion(child.attributes['offset']).clamp(0.0, 1.0)
|
144
150
|
|
145
151
|
# Offsets must be strictly increasing (SVG 13.2.4)
|
146
|
-
offset = result.last
|
152
|
+
offset = result.last[:offset] if result.last && result.last[:offset] > offset
|
147
153
|
|
148
|
-
if (color =
|
149
|
-
result <<
|
154
|
+
if (color = child.properties.stop_color&.value)
|
155
|
+
result << { offset: offset, color: color, opacity: (child.properties.stop_opacity || 1.0).clamp(0.0, 1.0) }
|
150
156
|
end
|
151
157
|
end
|
152
158
|
|
@@ -157,17 +163,36 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
157
163
|
|
158
164
|
@stops = parent_gradient.stops
|
159
165
|
else
|
160
|
-
|
161
|
-
|
166
|
+
if stops.first[:offset].positive?
|
167
|
+
start_stop = stops.first.dup
|
168
|
+
start_stop[:offset] = 0
|
169
|
+
stops.unshift(start_stop)
|
170
|
+
end
|
171
|
+
|
172
|
+
if stops.last[:offset] < 1
|
173
|
+
end_stop = stops.last.dup
|
174
|
+
end_stop[:offset] = 1
|
175
|
+
stops.push(end_stop)
|
176
|
+
end
|
162
177
|
end
|
163
178
|
end
|
164
179
|
|
165
|
-
def
|
180
|
+
def percentage_or_proportion(string, default = 0)
|
166
181
|
string = string.to_s.strip
|
167
|
-
|
182
|
+
percentage = false
|
183
|
+
|
184
|
+
if string[-1] == '%'
|
185
|
+
percentage = true
|
186
|
+
string = string[0..-2]
|
187
|
+
end
|
168
188
|
|
169
|
-
value = string
|
170
|
-
|
171
|
-
|
189
|
+
value = Float(string, exception: false)
|
190
|
+
return default unless value
|
191
|
+
|
192
|
+
if percentage
|
193
|
+
value / 100.0
|
194
|
+
else
|
195
|
+
value
|
196
|
+
end
|
172
197
|
end
|
173
198
|
end
|
@@ -3,7 +3,7 @@ class Prawn::SVG::Elements::Line < Prawn::SVG::Elements::Base
|
|
3
3
|
|
4
4
|
def parse
|
5
5
|
# Lines are one dimensional, so cannot be filled.
|
6
|
-
computed_properties.fill =
|
6
|
+
computed_properties.fill = Prawn::SVG::Paint.none
|
7
7
|
|
8
8
|
@x1 = x_pixels(attributes['x1'] || 0)
|
9
9
|
@y1 = y_pixels(attributes['y1'] || 0)
|
@@ -56,6 +56,8 @@ class Prawn::SVG::Elements::Marker < Prawn::SVG::Elements::Base
|
|
56
56
|
|
57
57
|
new_state = state.dup
|
58
58
|
new_state.computed_properties = computed_properties.dup
|
59
|
+
new_state.last_fill_opacity = element.state.last_fill_opacity
|
60
|
+
new_state.last_stroke_opacity = element.state.last_stroke_opacity
|
59
61
|
|
60
62
|
container = Prawn::SVG::Elements::Container.new(document, nil, [], new_state)
|
61
63
|
container.properties.compute_properties(new_state.computed_properties)
|
@@ -8,7 +8,7 @@ class Prawn::SVG::Elements::Root < Prawn::SVG::Elements::Base
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def apply
|
11
|
-
if [nil, 'inherit'
|
11
|
+
if [nil, 'inherit'].include?(properties.fill) || properties.fill.none? || properties.fill.color == :currentcolor
|
12
12
|
add_call 'fill_color', Prawn::SVG::Color.default_color(@document.color_mode).value
|
13
13
|
end
|
14
14
|
|
@@ -46,7 +46,7 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
|
|
46
46
|
# text_anchor and dominant_baseline aren't Prawn options; we have to do some math to support them
|
47
47
|
# and so we handle them in Prawn::SVG::Interface#rewrite_call_arguments
|
48
48
|
opts = {
|
49
|
-
size: computed_properties.
|
49
|
+
size: computed_properties.numeric_font_size,
|
50
50
|
style: font&.subfamily,
|
51
51
|
text_anchor: computed_properties.text_anchor
|
52
52
|
}
|
@@ -222,8 +222,8 @@ class Prawn::SVG::Elements::TextComponent < Prawn::SVG::Elements::DepthFirstBase
|
|
222
222
|
end
|
223
223
|
|
224
224
|
def calculate_text_rendering_mode
|
225
|
-
fill = computed_properties.fill
|
226
|
-
stroke = computed_properties.stroke
|
225
|
+
fill = !computed_properties.fill.none? # rubocop:disable Style/InverseMethods
|
226
|
+
stroke = !computed_properties.stroke.none? # rubocop:disable Style/InverseMethods
|
227
227
|
|
228
228
|
if fill && stroke
|
229
229
|
:fill_stroke
|
@@ -0,0 +1,313 @@
|
|
1
|
+
class Prawn::SVG::GradientRenderer
|
2
|
+
include Prawn::SVG::PDFMatrix
|
3
|
+
|
4
|
+
@mutex = Mutex.new
|
5
|
+
@counter = 0
|
6
|
+
|
7
|
+
def initialize(prawn, draw_type, from:, to:, stops:, matrix: nil, r1: nil, r2: nil, wrap: :pad, bounding_box: nil)
|
8
|
+
@prawn = prawn
|
9
|
+
@draw_type = draw_type
|
10
|
+
@from = from
|
11
|
+
@to = to
|
12
|
+
@bounding_box = bounding_box
|
13
|
+
|
14
|
+
if r1
|
15
|
+
@shading_type = 3
|
16
|
+
@coordinates = [*from, r1, *to, r2]
|
17
|
+
else
|
18
|
+
@shading_type = 2
|
19
|
+
@coordinates = [*from, *to]
|
20
|
+
end
|
21
|
+
|
22
|
+
@stop_offsets, @color_stops, @opacity_stops = process_stop_arguments(stops)
|
23
|
+
@gradient_matrix = matrix ? load_matrix(matrix) : Matrix.identity(3)
|
24
|
+
@wrap = wrap
|
25
|
+
end
|
26
|
+
|
27
|
+
def draw
|
28
|
+
key = self.class.next_key
|
29
|
+
|
30
|
+
# If we need transparency, add an ExtGState to the page and enable it.
|
31
|
+
if opacity_stops
|
32
|
+
prawn.page.ext_gstates["PSVG-ExtGState-#{key}"] = create_transparency_graphics_state
|
33
|
+
prawn.renderer.add_content("/PSVG-ExtGState-#{key} gs")
|
34
|
+
end
|
35
|
+
|
36
|
+
# Add pattern to the PDF page resources dictionary.
|
37
|
+
prawn.page.resources[:Pattern] ||= {}
|
38
|
+
prawn.page.resources[:Pattern]["PSVG-Pattern-#{key}"] = create_gradient_pattern
|
39
|
+
|
40
|
+
# Finally set the pattern with the drawing operator for fill/stroke.
|
41
|
+
prawn.send(:set_color_space, draw_type, :Pattern)
|
42
|
+
draw_operator = draw_type == :fill ? 'scn' : 'SCN'
|
43
|
+
prawn.renderer.add_content("/PSVG-Pattern-#{key} #{draw_operator}")
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.next_key
|
47
|
+
@mutex.synchronize { @counter += 1 }
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :prawn, :draw_type, :shading_type, :coordinates, :from, :to,
|
53
|
+
:stop_offsets, :color_stops, :opacity_stops, :gradient_matrix, :wrap, :bounding_box
|
54
|
+
|
55
|
+
def process_stop_arguments(stops)
|
56
|
+
stop_offsets = []
|
57
|
+
color_stops = []
|
58
|
+
opacity_stops = []
|
59
|
+
|
60
|
+
transparency = false
|
61
|
+
|
62
|
+
stops.each do |stop|
|
63
|
+
opacity = stop[:opacity] || 1.0
|
64
|
+
|
65
|
+
transparency = true if opacity < 1
|
66
|
+
|
67
|
+
stop_offsets << stop[:offset]
|
68
|
+
color_stops << prawn.send(:normalize_color, stop[:color])
|
69
|
+
opacity_stops << [opacity]
|
70
|
+
end
|
71
|
+
|
72
|
+
opacity_stops = nil unless transparency
|
73
|
+
|
74
|
+
[stop_offsets, color_stops, opacity_stops]
|
75
|
+
end
|
76
|
+
|
77
|
+
def create_transparency_graphics_state
|
78
|
+
prawn.renderer.min_version(1.4)
|
79
|
+
|
80
|
+
repeat_count, repeat_offset, transform = compute_wrapping(wrap, from, to, current_pdf_translation)
|
81
|
+
|
82
|
+
transparency_group = prawn.ref!(
|
83
|
+
Type: :XObject,
|
84
|
+
Subtype: :Form,
|
85
|
+
BBox: prawn.state.page.dimensions,
|
86
|
+
Group: {
|
87
|
+
Type: :Group,
|
88
|
+
S: :Transparency,
|
89
|
+
I: true,
|
90
|
+
CS: :DeviceGray
|
91
|
+
},
|
92
|
+
Resources: {
|
93
|
+
Pattern: {
|
94
|
+
'TGP01' => {
|
95
|
+
PatternType: 2,
|
96
|
+
Matrix: matrix_for_pdf(transform),
|
97
|
+
Shading: {
|
98
|
+
ShadingType: shading_type,
|
99
|
+
ColorSpace: :DeviceGray,
|
100
|
+
Coords: coordinates,
|
101
|
+
Domain: [0, repeat_count],
|
102
|
+
Function: create_shading_function(stop_offsets, opacity_stops, wrap, repeat_count, repeat_offset),
|
103
|
+
Extend: [true, true]
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
}
|
108
|
+
)
|
109
|
+
|
110
|
+
transparency_group.stream << begin
|
111
|
+
box = PDF::Core.real_params(prawn.state.page.dimensions)
|
112
|
+
|
113
|
+
<<~CMDS.strip
|
114
|
+
/Pattern cs
|
115
|
+
/TGP01 scn
|
116
|
+
#{box} re
|
117
|
+
f
|
118
|
+
CMDS
|
119
|
+
end
|
120
|
+
|
121
|
+
prawn.ref!(
|
122
|
+
Type: :ExtGState,
|
123
|
+
SMask: {
|
124
|
+
Type: :Mask,
|
125
|
+
S: :Luminosity,
|
126
|
+
G: transparency_group
|
127
|
+
},
|
128
|
+
AIS: false
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
def create_gradient_pattern
|
133
|
+
repeat_count, repeat_offset, transform = compute_wrapping(wrap, from, to, current_pdf_transform)
|
134
|
+
|
135
|
+
prawn.ref!(
|
136
|
+
PatternType: 2,
|
137
|
+
Matrix: matrix_for_pdf(transform),
|
138
|
+
Shading: {
|
139
|
+
ShadingType: shading_type,
|
140
|
+
ColorSpace: prawn.send(:color_space, color_stops.first),
|
141
|
+
Coords: coordinates,
|
142
|
+
Domain: [0, repeat_count],
|
143
|
+
Function: create_shading_function(stop_offsets, color_stops, wrap, repeat_count, repeat_offset),
|
144
|
+
Extend: [true, true]
|
145
|
+
}
|
146
|
+
)
|
147
|
+
end
|
148
|
+
|
149
|
+
def create_shading_function(offsets, stop_values, wrap = :pad, repeat_count = 1, repeat_offset = 0)
|
150
|
+
gradient_func = create_shading_function_for_stops(offsets, stop_values)
|
151
|
+
|
152
|
+
# Return the gradient function if there is no need to repeat.
|
153
|
+
return gradient_func if wrap == :pad
|
154
|
+
|
155
|
+
even_odd_encode = wrap == :reflect ? [[0, 1], [1, 0]] : [[0, 1], [0, 1]]
|
156
|
+
encode = repeat_count.times.flat_map { |num| even_odd_encode[(num + repeat_offset) % 2] }
|
157
|
+
|
158
|
+
prawn.ref!(
|
159
|
+
FunctionType: 3, # stitching function
|
160
|
+
Domain: [0, repeat_count],
|
161
|
+
Functions: Array.new(repeat_count, gradient_func),
|
162
|
+
Bounds: Range.new(1, repeat_count - 1).to_a,
|
163
|
+
Encode: encode
|
164
|
+
)
|
165
|
+
end
|
166
|
+
|
167
|
+
def create_shading_function_for_stops(offsets, stop_values)
|
168
|
+
linear_funcs = stop_values.each_cons(2).map do |c0, c1|
|
169
|
+
prawn.ref!(FunctionType: 2, Domain: [0.0, 1.0], C0: c0, C1: c1, N: 1.0)
|
170
|
+
end
|
171
|
+
|
172
|
+
# If there's only two stops, we can use the single shader.
|
173
|
+
return linear_funcs.first if linear_funcs.length == 1
|
174
|
+
|
175
|
+
# Otherwise we stitch the multiple shaders together.
|
176
|
+
prawn.ref!(
|
177
|
+
FunctionType: 3, # stitching function
|
178
|
+
Domain: [0.0, 1.0],
|
179
|
+
Functions: linear_funcs,
|
180
|
+
Bounds: offsets[1..-2],
|
181
|
+
Encode: [0.0, 1.0] * linear_funcs.length
|
182
|
+
)
|
183
|
+
end
|
184
|
+
|
185
|
+
def current_pdf_transform
|
186
|
+
@current_pdf_transform ||= load_matrix(
|
187
|
+
prawn.current_transformation_matrix_with_translation(*prawn.bounds.anchor)
|
188
|
+
)
|
189
|
+
end
|
190
|
+
|
191
|
+
def current_pdf_translation
|
192
|
+
@current_pdf_translation ||= begin
|
193
|
+
bounds_x, bounds_y = prawn.bounds.anchor
|
194
|
+
Matrix[[1, 0, bounds_x], [0, 1, bounds_y], [0, 0, 1]]
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def bounding_box_corners(matrix)
|
199
|
+
if bounding_box
|
200
|
+
transformed_corners(gradient_matrix.inverse, *bounding_box)
|
201
|
+
else
|
202
|
+
transformed_corners(matrix.inverse, *prawn_bounding_box)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def prawn_bounding_box
|
207
|
+
[*prawn.bounds.top_left, *prawn.bounds.bottom_right]
|
208
|
+
end
|
209
|
+
|
210
|
+
def transformed_corners(matrix, left, top, right, bottom)
|
211
|
+
[
|
212
|
+
matrix * Vector[left, top, 1.0],
|
213
|
+
matrix * Vector[left, bottom, 1.0],
|
214
|
+
matrix * Vector[right, top, 1.0],
|
215
|
+
matrix * Vector[right, bottom, 1.0]
|
216
|
+
]
|
217
|
+
end
|
218
|
+
|
219
|
+
def compute_wrapping(wrap, from, to, page_transform)
|
220
|
+
matrix = page_transform * gradient_matrix
|
221
|
+
|
222
|
+
return [1, 0, matrix] if wrap == :pad
|
223
|
+
|
224
|
+
from = Vector[from[0], from[1], 1.0]
|
225
|
+
to = Vector[to[0], to[1], 1.0]
|
226
|
+
|
227
|
+
# Transform the bounding box into gradient space where lines are straight
|
228
|
+
# and circles are round.
|
229
|
+
box_corners = bounding_box_corners(matrix)
|
230
|
+
|
231
|
+
repeat_count, repeat_offset, delta = if shading_type == 2 # Linear
|
232
|
+
project_bounding_box_for_linear(from, to, box_corners)
|
233
|
+
else # Radial
|
234
|
+
project_bounding_box_for_radial(from, to, box_corners)
|
235
|
+
end
|
236
|
+
|
237
|
+
repeat_count = [repeat_count, 50].min
|
238
|
+
|
239
|
+
wrap_transform = translation_matrix(delta[0], delta[1]) *
|
240
|
+
translation_matrix(from[0], from[1]) *
|
241
|
+
scale_matrix(repeat_count) *
|
242
|
+
translation_matrix(-from[0], -from[1])
|
243
|
+
|
244
|
+
[repeat_count, repeat_offset, matrix * wrap_transform]
|
245
|
+
end
|
246
|
+
|
247
|
+
def project_bounding_box_for_linear(from, to, box_corners)
|
248
|
+
ab = to - from
|
249
|
+
|
250
|
+
# Project each corner of the bounding box onto the line made by the
|
251
|
+
# gradient. The formula for projecting a point C onto a line formed from
|
252
|
+
# point A to point B is as follows:
|
253
|
+
#
|
254
|
+
# AB = B - A
|
255
|
+
# AC = C - A
|
256
|
+
# t = (AB dot AC) / (AB dot AB)
|
257
|
+
# P = A + (AB * t)
|
258
|
+
#
|
259
|
+
# We don't actually need the final point P, we only need the parameter "t",
|
260
|
+
# so that we know how many times to repeat the gradient.
|
261
|
+
t_for_corners = box_corners.map do |corner|
|
262
|
+
ac = corner - from
|
263
|
+
ab.dot(ac) / ab.dot(ab)
|
264
|
+
end
|
265
|
+
|
266
|
+
t_min, t_max = t_for_corners.minmax
|
267
|
+
|
268
|
+
repeat_count = (t_max - t_min).ceil + 1
|
269
|
+
|
270
|
+
shift_count = t_min.floor
|
271
|
+
delta = ab * shift_count
|
272
|
+
repeat_offset = shift_count % 2
|
273
|
+
|
274
|
+
[repeat_count, repeat_offset, delta]
|
275
|
+
end
|
276
|
+
|
277
|
+
def project_bounding_box_for_radial(from, to, box_corners)
|
278
|
+
r1 = coordinates[2]
|
279
|
+
r2 = coordinates[5]
|
280
|
+
|
281
|
+
# For radial gradients, the approach is similar. We need to find "t" to
|
282
|
+
# know how far along the gradient line each corner of the bounding box
|
283
|
+
# lies. Only this time we need to solve the simultaneous equation for the
|
284
|
+
# point on both inner and outer circles.
|
285
|
+
#
|
286
|
+
# You can find the derivation for this here:
|
287
|
+
# https://github.com/libpixman/pixman/blob/85467ec308f8621a5410c007491797b7b1847601/pixman/pixman-radial-gradient.c#L162-L241
|
288
|
+
#
|
289
|
+
# Do this for all 4 corners and pick the biggest number to repeat.
|
290
|
+
t = box_corners.reduce(1) do |max, corner|
|
291
|
+
cdx, cdy = *(to - from)
|
292
|
+
pdx, pdy = *(corner - from)
|
293
|
+
dr = r2 - r1
|
294
|
+
|
295
|
+
a = cdx.abs2 + cdy.abs2 - dr.abs2
|
296
|
+
b = (pdx * cdx) + (pdy * cdy) + (r1 * dr)
|
297
|
+
c = pdx.abs2 + pdy.abs2 - r1.abs2
|
298
|
+
det_root = Math.sqrt(b.abs2 - (a * c))
|
299
|
+
|
300
|
+
t0 = (b + det_root) / a
|
301
|
+
t1 = (b - det_root) / a
|
302
|
+
|
303
|
+
[t0, t1, max].max
|
304
|
+
end
|
305
|
+
|
306
|
+
repeat_count = t.ceil
|
307
|
+
|
308
|
+
delta = [0.0, 0.0]
|
309
|
+
repeat_offset = 0
|
310
|
+
|
311
|
+
[repeat_count, repeat_offset, delta]
|
312
|
+
end
|
313
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Prawn::SVG
|
2
|
+
Length = Struct.new(:value, :unit)
|
3
|
+
|
4
|
+
class Length
|
5
|
+
REGEXP = /\A([+-]?\d*(?:\.\d+)?)(em|rem|ex|px|in|cm|mm|pt|pc)?\z/i.freeze
|
6
|
+
|
7
|
+
def self.parse(value, positive_only: false)
|
8
|
+
if (matches = value.match(REGEXP))
|
9
|
+
number = Float(matches[1], exception: false)
|
10
|
+
new(number, matches[2]&.downcase&.to_sym) if number && (!positive_only || number >= 0)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_f
|
15
|
+
value
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"#{value}#{unit}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_pixels(_axis_length, font_size)
|
23
|
+
case unit
|
24
|
+
when :em
|
25
|
+
value * font_size
|
26
|
+
when :rem
|
27
|
+
value * Properties::EM
|
28
|
+
when :ex
|
29
|
+
value * (font_size / 2.0) # we don't have access to the x-height, so this is an approximation approved by the CSS spec
|
30
|
+
when :pc
|
31
|
+
value * 15 # according to http://www.w3.org/TR/SVG11/coords.html
|
32
|
+
when :in
|
33
|
+
value * 72
|
34
|
+
when :cm
|
35
|
+
value * 10 * (72 / 25.4)
|
36
|
+
when :mm
|
37
|
+
value * (72 / 25.4)
|
38
|
+
else
|
39
|
+
value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|