prawn-svg 0.35.0 → 0.36.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/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 +25 -7
- 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
|