prawnx 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/COPYING +2 -0
- data/GPLv2 +339 -0
- data/GPLv3 +674 -0
- data/LICENSE +65 -0
- data/LICENSE-prawn-svg +21 -0
- data/README.md +107 -0
- data/data/fonts/Courier-Bold.afm +342 -0
- data/data/fonts/Courier-BoldOblique.afm +342 -0
- data/data/fonts/Courier-Oblique.afm +342 -0
- data/data/fonts/Courier.afm +342 -0
- data/data/fonts/Helvetica-Bold.afm +2827 -0
- data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
- data/data/fonts/Helvetica-Oblique.afm +3051 -0
- data/data/fonts/Helvetica.afm +3051 -0
- data/data/fonts/MustRead.html +19 -0
- data/data/fonts/Symbol.afm +213 -0
- data/data/fonts/Times-Bold.afm +2588 -0
- data/data/fonts/Times-BoldItalic.afm +2384 -0
- data/data/fonts/Times-Italic.afm +2667 -0
- data/data/fonts/Times-Roman.afm +2419 -0
- data/data/fonts/ZapfDingbats.afm +225 -0
- data/lib/pdf/core.rb +4 -0
- data/lib/prawn/measurement_extensions.rb +4 -0
- data/lib/prawn/svg/attributes/clip_path.rb +46 -0
- data/lib/prawn/svg/attributes/mask.rb +16 -0
- data/lib/prawn/svg/attributes/opacity.rb +30 -0
- data/lib/prawn/svg/attributes/space.rb +10 -0
- data/lib/prawn/svg/attributes/stroke.rb +50 -0
- data/lib/prawn/svg/attributes/transform.rb +10 -0
- data/lib/prawn/svg/attributes.rb +6 -0
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
- data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
- data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
- data/lib/prawn/svg/calculators/path_length.rb +168 -0
- data/lib/prawn/svg/calculators/pixels.rb +57 -0
- data/lib/prawn/svg/color.rb +240 -0
- data/lib/prawn/svg/css/font_face_parser.rb +72 -0
- data/lib/prawn/svg/css/font_family_parser.rb +39 -0
- data/lib/prawn/svg/css/font_parser.rb +46 -0
- data/lib/prawn/svg/css/selector_parser.rb +178 -0
- data/lib/prawn/svg/css/stylesheets.rb +233 -0
- data/lib/prawn/svg/css/values_parser.rb +68 -0
- data/lib/prawn/svg/document.rb +170 -0
- data/lib/prawn/svg/elements/anchor.rb +10 -0
- data/lib/prawn/svg/elements/base.rb +312 -0
- data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
- data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
- data/lib/prawn/svg/elements/circle.rb +25 -0
- data/lib/prawn/svg/elements/clip_path.rb +26 -0
- data/lib/prawn/svg/elements/container.rb +10 -0
- data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
- data/lib/prawn/svg/elements/ellipse.rb +22 -0
- data/lib/prawn/svg/elements/gradient.rb +184 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +177 -0
- data/lib/prawn/svg/elements/line.rb +27 -0
- data/lib/prawn/svg/elements/marker.rb +72 -0
- data/lib/prawn/svg/elements/mask.rb +65 -0
- data/lib/prawn/svg/elements/path.rb +294 -0
- data/lib/prawn/svg/elements/pattern.rb +150 -0
- data/lib/prawn/svg/elements/polygon.rb +25 -0
- data/lib/prawn/svg/elements/polyline.rb +23 -0
- data/lib/prawn/svg/elements/rect.rb +64 -0
- data/lib/prawn/svg/elements/root.rb +22 -0
- data/lib/prawn/svg/elements/switch.rb +92 -0
- data/lib/prawn/svg/elements/text.rb +99 -0
- data/lib/prawn/svg/elements/text_component.rb +346 -0
- data/lib/prawn/svg/elements/text_node.rb +308 -0
- data/lib/prawn/svg/elements/text_path.rb +153 -0
- data/lib/prawn/svg/elements/use.rb +142 -0
- data/lib/prawn/svg/elements/viewport.rb +29 -0
- data/lib/prawn/svg/elements.rb +46 -0
- data/lib/prawn/svg/extension.rb +25 -0
- data/lib/prawn/svg/font.rb +20 -0
- data/lib/prawn/svg/font_metrics.rb +144 -0
- data/lib/prawn/svg/font_registry.rb +315 -0
- data/lib/prawn/svg/funciri.rb +14 -0
- data/lib/prawn/svg/gradient_renderer.rb +332 -0
- data/lib/prawn/svg/gradients.rb +53 -0
- data/lib/prawn/svg/interface.rb +72 -0
- data/lib/prawn/svg/length.rb +43 -0
- data/lib/prawn/svg/link_renderer.rb +40 -0
- data/lib/prawn/svg/loaders/data.rb +17 -0
- data/lib/prawn/svg/loaders/file.rb +111 -0
- data/lib/prawn/svg/loaders/web.rb +24 -0
- data/lib/prawn/svg/paint.rb +67 -0
- data/lib/prawn/svg/pathable.rb +153 -0
- data/lib/prawn/svg/pattern_renderer.rb +112 -0
- data/lib/prawn/svg/pdfmatrix.rb +37 -0
- data/lib/prawn/svg/percentage.rb +24 -0
- data/lib/prawn/svg/properties.rb +329 -0
- data/lib/prawn/svg/renderer.rb +249 -0
- data/lib/prawn/svg/state.rb +24 -0
- data/lib/prawn/svg/transform_parser.rb +78 -0
- data/lib/prawn/svg/ttc.rb +35 -0
- data/lib/prawn/svg/ttf.rb +95 -0
- data/lib/prawn/svg/url_loader.rb +46 -0
- data/lib/prawn/svg/version.rb +5 -0
- data/lib/prawn/table.rb +4 -0
- data/lib/prawn-svg.rb +53 -0
- data/lib/prawn.rb +10 -0
- data/lib/prawnx/annotations.rb +21 -0
- data/lib/prawnx/document/bounding_box.rb +193 -0
- data/lib/prawnx/document/column_box.rb +91 -0
- data/lib/prawnx/document/internals.rb +144 -0
- data/lib/prawnx/document/span.rb +25 -0
- data/lib/prawnx/document.rb +253 -0
- data/lib/prawnx/encoding.rb +29 -0
- data/lib/prawnx/errors.rb +37 -0
- data/lib/prawnx/font.rb +214 -0
- data/lib/prawnx/fonts/afm.rb +171 -0
- data/lib/prawnx/fonts/otf.rb +50 -0
- data/lib/prawnx/fonts/ttf.rb +410 -0
- data/lib/prawnx/graphic_state.rb +41 -0
- data/lib/prawnx/graphics/color.rb +148 -0
- data/lib/prawnx/graphics/patterns.rb +100 -0
- data/lib/prawnx/graphics/style.rb +171 -0
- data/lib/prawnx/graphics.rb +208 -0
- data/lib/prawnx/grid.rb +216 -0
- data/lib/prawnx/images/image.rb +244 -0
- data/lib/prawnx/images/png_decoder.rb +65 -0
- data/lib/prawnx/images.rb +115 -0
- data/lib/prawnx/measurement_extensions.rb +40 -0
- data/lib/prawnx/measurements.rb +62 -0
- data/lib/prawnx/outline.rb +146 -0
- data/lib/prawnx/page.rb +125 -0
- data/lib/prawnx/page_geometry.rb +27 -0
- data/lib/prawnx/pdf/outline_node.rb +53 -0
- data/lib/prawnx/pdf/reference.rb +31 -0
- data/lib/prawnx/pdf/writer.rb +105 -0
- data/lib/prawnx/pdf.rb +66 -0
- data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
- data/lib/prawnx/pdf_a.rb +130 -0
- data/lib/prawnx/pdf_core.rb +25 -0
- data/lib/prawnx/repeater.rb +79 -0
- data/lib/prawnx/security.rb +108 -0
- data/lib/prawnx/stamp.rb +74 -0
- data/lib/prawnx/table/cell/kinds.rb +246 -0
- data/lib/prawnx/table/cell.rb +331 -0
- data/lib/prawnx/table/cells.rb +208 -0
- data/lib/prawnx/table.rb +296 -0
- data/lib/prawnx/text/box.rb +16 -0
- data/lib/prawnx/text/formatted/arranger.rb +217 -0
- data/lib/prawnx/text/formatted/box.rb +312 -0
- data/lib/prawnx/text/formatted/fragment.rb +205 -0
- data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
- data/lib/prawnx/text/formatted/parser.rb +149 -0
- data/lib/prawnx/text/formatted/wrap.rb +103 -0
- data/lib/prawnx/text.rb +281 -0
- data/lib/prawnx/transformation_stack.rb +43 -0
- data/lib/prawnx/true_type/cff_subset.rb +157 -0
- data/lib/prawnx/true_type/file.rb +371 -0
- data/lib/prawnx/true_type/open_type_layout.rb +189 -0
- data/lib/prawnx/true_type/simple_subsets.rb +94 -0
- data/lib/prawnx/true_type/subset.rb +187 -0
- data/lib/prawnx/version.rb +7 -0
- data/lib/prawnx/view.rb +28 -0
- data/lib/prawnx.rb +102 -0
- metadata +251 -0
|
@@ -0,0 +1,332 @@
|
|
|
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
|
+
x0, y0, x1, y1 = bounding_box || prawn_bounding_box
|
|
83
|
+
|
|
84
|
+
offset_x, offset_y = prawn.bounds.anchor
|
|
85
|
+
x0 += offset_x
|
|
86
|
+
y0 += offset_y
|
|
87
|
+
x1 += offset_x
|
|
88
|
+
y1 += offset_y
|
|
89
|
+
|
|
90
|
+
width = x1 - x0
|
|
91
|
+
height = y0 - y1
|
|
92
|
+
|
|
93
|
+
transparency_group = prawn.ref!(
|
|
94
|
+
Type: :XObject,
|
|
95
|
+
Subtype: :Form,
|
|
96
|
+
BBox: [x0, y1, x1, y0],
|
|
97
|
+
Group: {
|
|
98
|
+
Type: :Group,
|
|
99
|
+
S: :Transparency,
|
|
100
|
+
I: true,
|
|
101
|
+
CS: :DeviceGray
|
|
102
|
+
},
|
|
103
|
+
Resources: {
|
|
104
|
+
Pattern: {
|
|
105
|
+
'TGP01' => {
|
|
106
|
+
PatternType: 2,
|
|
107
|
+
Matrix: matrix_for_pdf(transform),
|
|
108
|
+
Shading: {
|
|
109
|
+
ShadingType: shading_type,
|
|
110
|
+
ColorSpace: :DeviceGray,
|
|
111
|
+
Coords: coordinates,
|
|
112
|
+
Domain: [0, repeat_count],
|
|
113
|
+
Function: create_shading_function(stop_offsets, opacity_stops, wrap, repeat_count, repeat_offset),
|
|
114
|
+
Extend: [true, true]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
transparency_group.stream << begin
|
|
122
|
+
box = PDF::Core.real_params([x0, y1, width, height])
|
|
123
|
+
|
|
124
|
+
<<~CMDS.strip
|
|
125
|
+
/Pattern cs
|
|
126
|
+
/TGP01 scn
|
|
127
|
+
#{box} re
|
|
128
|
+
f
|
|
129
|
+
CMDS
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
prawn.ref!(
|
|
133
|
+
Type: :ExtGState,
|
|
134
|
+
SMask: {
|
|
135
|
+
Type: :Mask,
|
|
136
|
+
S: :Luminosity,
|
|
137
|
+
G: transparency_group
|
|
138
|
+
},
|
|
139
|
+
AIS: false
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def create_gradient_pattern
|
|
144
|
+
repeat_count, repeat_offset, transform = compute_wrapping(wrap, from, to, current_pdf_transform)
|
|
145
|
+
|
|
146
|
+
prawn.ref!(
|
|
147
|
+
PatternType: 2,
|
|
148
|
+
Matrix: matrix_for_pdf(transform),
|
|
149
|
+
Shading: {
|
|
150
|
+
ShadingType: shading_type,
|
|
151
|
+
ColorSpace: prawn.send(:color_space, color_stops.first),
|
|
152
|
+
Coords: coordinates,
|
|
153
|
+
Domain: [0, repeat_count],
|
|
154
|
+
Function: create_shading_function(stop_offsets, color_stops, wrap, repeat_count, repeat_offset),
|
|
155
|
+
Extend: [true, true]
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def create_shading_function(offsets, stop_values, wrap = :pad, repeat_count = 1, repeat_offset = 0)
|
|
161
|
+
gradient_func = create_shading_function_for_stops(offsets, stop_values)
|
|
162
|
+
|
|
163
|
+
# Return the gradient function if there is no need to repeat.
|
|
164
|
+
return gradient_func if wrap == :pad
|
|
165
|
+
|
|
166
|
+
even_odd_encode = wrap == :reflect ? [[0, 1], [1, 0]] : [[0, 1], [0, 1]]
|
|
167
|
+
encode = repeat_count.times.flat_map { |num| even_odd_encode[(num + repeat_offset) % 2] }
|
|
168
|
+
|
|
169
|
+
prawn.ref!(
|
|
170
|
+
FunctionType: 3, # stitching function
|
|
171
|
+
Domain: [0, repeat_count],
|
|
172
|
+
Functions: Array.new(repeat_count, gradient_func),
|
|
173
|
+
Bounds: Range.new(1, repeat_count - 1).to_a,
|
|
174
|
+
Encode: encode
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def create_shading_function_for_stops(offsets, stop_values)
|
|
179
|
+
linear_funcs = stop_values.each_cons(2).map do |c0, c1|
|
|
180
|
+
prawn.ref!(FunctionType: 2, Domain: [0.0, 1.0], C0: c0, C1: c1, N: 1.0)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# If there's only two stops, we can use the single shader.
|
|
184
|
+
return linear_funcs.first if linear_funcs.length == 1
|
|
185
|
+
|
|
186
|
+
# Otherwise we stitch the multiple shaders together.
|
|
187
|
+
prawn.ref!(
|
|
188
|
+
FunctionType: 3, # stitching function
|
|
189
|
+
Domain: [0.0, 1.0],
|
|
190
|
+
Functions: linear_funcs,
|
|
191
|
+
Bounds: offsets[1..-2],
|
|
192
|
+
Encode: [0.0, 1.0] * linear_funcs.length
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def current_pdf_transform
|
|
197
|
+
@current_pdf_transform ||= if prawn.state.page.in_stamp_stream?
|
|
198
|
+
# Inside a soft_mask Form XObject, the CTM resets to identity but Prawn's
|
|
199
|
+
# internal tracking still reflects the page CTM. The geometry coordinates
|
|
200
|
+
# only have the bounds offset applied (no scaling), so the pattern matrix
|
|
201
|
+
# should match by using only the bounds translation.
|
|
202
|
+
current_pdf_translation
|
|
203
|
+
else
|
|
204
|
+
load_matrix(
|
|
205
|
+
prawn.current_transformation_matrix_with_translation(*prawn.bounds.anchor)
|
|
206
|
+
)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def current_pdf_translation
|
|
211
|
+
@current_pdf_translation ||= begin
|
|
212
|
+
bounds_x, bounds_y = prawn.bounds.anchor
|
|
213
|
+
Matrix[[1, 0, bounds_x], [0, 1, bounds_y], [0, 0, 1]]
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def bounding_box_corners(matrix)
|
|
218
|
+
if bounding_box
|
|
219
|
+
transformed_corners(gradient_matrix.inverse, *bounding_box)
|
|
220
|
+
else
|
|
221
|
+
transformed_corners(matrix.inverse, *prawn_bounding_box)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def prawn_bounding_box
|
|
226
|
+
[*prawn.bounds.top_left, *prawn.bounds.bottom_right]
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def transformed_corners(matrix, left, top, right, bottom)
|
|
230
|
+
[
|
|
231
|
+
matrix * Vector[left, top, 1.0],
|
|
232
|
+
matrix * Vector[left, bottom, 1.0],
|
|
233
|
+
matrix * Vector[right, top, 1.0],
|
|
234
|
+
matrix * Vector[right, bottom, 1.0]
|
|
235
|
+
]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def compute_wrapping(wrap, from, to, page_transform)
|
|
239
|
+
matrix = page_transform * gradient_matrix
|
|
240
|
+
|
|
241
|
+
return [1, 0, matrix] if wrap == :pad
|
|
242
|
+
|
|
243
|
+
from = Vector[from[0], from[1], 1.0]
|
|
244
|
+
to = Vector[to[0], to[1], 1.0]
|
|
245
|
+
|
|
246
|
+
# Transform the bounding box into gradient space where lines are straight
|
|
247
|
+
# and circles are round.
|
|
248
|
+
box_corners = bounding_box_corners(matrix)
|
|
249
|
+
|
|
250
|
+
repeat_count, repeat_offset, delta = if shading_type == 2 # Linear
|
|
251
|
+
project_bounding_box_for_linear(from, to, box_corners)
|
|
252
|
+
else # Radial
|
|
253
|
+
project_bounding_box_for_radial(from, to, box_corners)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
repeat_count = [repeat_count, 50].min
|
|
257
|
+
|
|
258
|
+
wrap_transform = translation_matrix(delta[0], delta[1]) *
|
|
259
|
+
translation_matrix(from[0], from[1]) *
|
|
260
|
+
scale_matrix(repeat_count) *
|
|
261
|
+
translation_matrix(-from[0], -from[1])
|
|
262
|
+
|
|
263
|
+
[repeat_count, repeat_offset, matrix * wrap_transform]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def project_bounding_box_for_linear(from, to, box_corners)
|
|
267
|
+
ab = to - from
|
|
268
|
+
|
|
269
|
+
# Project each corner of the bounding box onto the line made by the
|
|
270
|
+
# gradient. The formula for projecting a point C onto a line formed from
|
|
271
|
+
# point A to point B is as follows:
|
|
272
|
+
#
|
|
273
|
+
# AB = B - A
|
|
274
|
+
# AC = C - A
|
|
275
|
+
# t = (AB dot AC) / (AB dot AB)
|
|
276
|
+
# P = A + (AB * t)
|
|
277
|
+
#
|
|
278
|
+
# We don't actually need the final point P, we only need the parameter "t",
|
|
279
|
+
# so that we know how many times to repeat the gradient.
|
|
280
|
+
t_for_corners = box_corners.map do |corner|
|
|
281
|
+
ac = corner - from
|
|
282
|
+
ab.dot(ac) / ab.dot(ab)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
t_min, t_max = t_for_corners.minmax
|
|
286
|
+
|
|
287
|
+
repeat_count = (t_max - t_min).ceil + 1
|
|
288
|
+
|
|
289
|
+
shift_count = t_min.floor
|
|
290
|
+
delta = ab * shift_count
|
|
291
|
+
repeat_offset = shift_count % 2
|
|
292
|
+
|
|
293
|
+
[repeat_count, repeat_offset, delta]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def project_bounding_box_for_radial(from, to, box_corners)
|
|
297
|
+
r1 = coordinates[2]
|
|
298
|
+
r2 = coordinates[5]
|
|
299
|
+
|
|
300
|
+
# For radial gradients, the approach is similar. We need to find "t" to
|
|
301
|
+
# know how far along the gradient line each corner of the bounding box
|
|
302
|
+
# lies. Only this time we need to solve the simultaneous equation for the
|
|
303
|
+
# point on both inner and outer circles.
|
|
304
|
+
#
|
|
305
|
+
# You can find the derivation for this here:
|
|
306
|
+
# https://github.com/libpixman/pixman/blob/85467ec308f8621a5410c007491797b7b1847601/pixman/pixman-radial-gradient.c#L162-L241
|
|
307
|
+
#
|
|
308
|
+
# Do this for all 4 corners and pick the biggest number to repeat.
|
|
309
|
+
t = box_corners.reduce(1) do |max, corner|
|
|
310
|
+
cdx, cdy = *(to - from)
|
|
311
|
+
pdx, pdy = *(corner - from)
|
|
312
|
+
dr = r2 - r1
|
|
313
|
+
|
|
314
|
+
a = cdx.abs2 + cdy.abs2 - dr.abs2
|
|
315
|
+
b = (pdx * cdx) + (pdy * cdy) + (r1 * dr)
|
|
316
|
+
c = pdx.abs2 + pdy.abs2 - r1.abs2
|
|
317
|
+
det_root = Math.sqrt(b.abs2 - (a * c))
|
|
318
|
+
|
|
319
|
+
t0 = (b + det_root) / a
|
|
320
|
+
t1 = (b - det_root) / a
|
|
321
|
+
|
|
322
|
+
[t0, t1, max].max
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
repeat_count = t.ceil
|
|
326
|
+
|
|
327
|
+
delta = [0.0, 0.0]
|
|
328
|
+
repeat_offset = 0
|
|
329
|
+
|
|
330
|
+
[repeat_count, repeat_offset, delta]
|
|
331
|
+
end
|
|
332
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Prawn::SVG
|
|
2
|
+
class Gradients
|
|
3
|
+
def initialize(document)
|
|
4
|
+
@document = document
|
|
5
|
+
@gradients_by_id = {}
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def [](id)
|
|
9
|
+
id &&= id.strip
|
|
10
|
+
return unless id && id != ''
|
|
11
|
+
|
|
12
|
+
if (element = @gradients_by_id[id])
|
|
13
|
+
element
|
|
14
|
+
elsif (raw_element = find_raw_gradient_element_by_id(id))
|
|
15
|
+
create_gradient_element(raw_element)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def []=(id, gradient)
|
|
20
|
+
@gradients_by_id[id] = gradient
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
PAINT_SERVER_CLASSES = [Elements::Gradient, Elements::Pattern].freeze
|
|
26
|
+
|
|
27
|
+
def find_raw_gradient_element_by_id(id)
|
|
28
|
+
raw_element = find_raw_element_by_id(id)
|
|
29
|
+
raw_element if gradient_element?(raw_element)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create_gradient_element(raw_element)
|
|
33
|
+
element_class = Elements::TAG_CLASS_MAPPING[raw_element.name.to_sym]
|
|
34
|
+
element_class.new(@document, raw_element, [], new_state).tap(&:process)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def find_raw_element_by_id(id)
|
|
38
|
+
REXML::XPath.match(@document.root, %(//*[@id="#{id.gsub('"', '\"')}"])).first
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def gradient_element?(raw_element)
|
|
42
|
+
return false if raw_element.nil? || raw_element.name.nil?
|
|
43
|
+
|
|
44
|
+
PAINT_SERVER_CLASSES.include?(Elements::TAG_CLASS_MAPPING[raw_element.name.to_sym])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def new_state
|
|
48
|
+
State.new.tap do |state|
|
|
49
|
+
state.viewport_sizing = @document.sizing
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Prawn::SVG::Interface makes a Prawn::SVG::Document instance, uses that object to parse the supplied
|
|
3
|
+
# SVG into Prawn-compatible method calls, and then calls the Prawn methods.
|
|
4
|
+
#
|
|
5
|
+
module Prawn
|
|
6
|
+
module SVG
|
|
7
|
+
class Interface
|
|
8
|
+
VALID_OPTIONS = %i[
|
|
9
|
+
at position vposition width height cache_images enable_web_requests
|
|
10
|
+
enable_file_requests_with_root fallback_font_name color_mode language
|
|
11
|
+
log_warnings
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
INHERITABLE_OPTIONS = %i[
|
|
15
|
+
enable_web_requests enable_file_requests_with_root
|
|
16
|
+
cache_images fallback_font_name color_mode
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :data, :prawn, :document, :options
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# Creates a Prawn::SVG object.
|
|
23
|
+
#
|
|
24
|
+
# +data+ is the SVG data to convert. +prawn+ is your Prawn::Document object.
|
|
25
|
+
#
|
|
26
|
+
# See README.md for the options that can be passed to this method.
|
|
27
|
+
#
|
|
28
|
+
def initialize(data, prawn, options, &block)
|
|
29
|
+
Prawn.verify_options VALID_OPTIONS, options
|
|
30
|
+
|
|
31
|
+
@data = data
|
|
32
|
+
@prawn = prawn
|
|
33
|
+
@options = options
|
|
34
|
+
|
|
35
|
+
font_registry = Prawn::SVG::FontRegistry.new(prawn.font_families)
|
|
36
|
+
|
|
37
|
+
@document = Document.new(
|
|
38
|
+
data, [prawn.bounds.width, prawn.bounds.height], options,
|
|
39
|
+
font_registry: font_registry, &block
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@renderer = Renderer.new(prawn, document, options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# Draws the SVG to the Prawn::Document object.
|
|
47
|
+
#
|
|
48
|
+
def draw
|
|
49
|
+
@renderer.draw
|
|
50
|
+
|
|
51
|
+
document.warnings.each { |w| warn w } if options[:log_warnings]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def sizing
|
|
55
|
+
document.sizing
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resize(width: nil, height: nil)
|
|
59
|
+
document.calculate_sizing(requested_width: width, requested_height: height)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def position
|
|
63
|
+
@renderer.position
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# backwards support for when the font_path used to be stored on this class
|
|
67
|
+
def self.font_path
|
|
68
|
+
Prawn::SVG::FontRegistry.font_path
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
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
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class Prawn::SVG::LinkRenderer
|
|
2
|
+
include Prawn::SVG::PDFMatrix
|
|
3
|
+
|
|
4
|
+
def initialize(href, bounding_box)
|
|
5
|
+
@href = href
|
|
6
|
+
@bounding_box = bounding_box
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def render(prawn)
|
|
10
|
+
prawn.link_annotation(transformed_bounding_box(prawn), {
|
|
11
|
+
Border: [0, 0, 0],
|
|
12
|
+
A: { Type: :Action, S: :URI, URI: PDF::Core::LiteralString.new(href) }
|
|
13
|
+
})
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :href, :bounding_box
|
|
19
|
+
|
|
20
|
+
def transformed_bounding_box(prawn)
|
|
21
|
+
x0, y0, x1, y1 = bounding_box
|
|
22
|
+
|
|
23
|
+
matrix = load_matrix(prawn.current_transformation_matrix_with_translation(*prawn.bounds.anchor))
|
|
24
|
+
|
|
25
|
+
corners = [
|
|
26
|
+
matrix * Vector[x0, y0, 1.0],
|
|
27
|
+
matrix * Vector[x0, y1, 1.0],
|
|
28
|
+
matrix * Vector[x1, y0, 1.0],
|
|
29
|
+
matrix * Vector[x1, y1, 1.0]
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
xs = corners.map { |c| c[0] }
|
|
33
|
+
ys = corners.map { |c| c[1] }
|
|
34
|
+
|
|
35
|
+
tx0, tx1 = xs.minmax
|
|
36
|
+
ty0, ty1 = ys.minmax
|
|
37
|
+
|
|
38
|
+
[tx0, ty0, tx1, ty1]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Prawn::SVG::Loaders
|
|
2
|
+
class Data
|
|
3
|
+
REGEXP = %r{\Adata:image/(png|jpeg|svg\+xml);base64(;[a-z0-9]+)*,}i.freeze
|
|
4
|
+
|
|
5
|
+
def from_url(url)
|
|
6
|
+
return if url[0..4].downcase != 'data:'
|
|
7
|
+
|
|
8
|
+
matches = url.match(REGEXP)
|
|
9
|
+
if matches.nil?
|
|
10
|
+
raise Prawn::SVG::UrlLoader::Error,
|
|
11
|
+
'prawn-svg only supports base64-encoded image/png, image/jpeg, and image/svg+xml data URLs'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
matches.post_match.unpack1('m')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require 'addressable/uri'
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Load a file from disk.
|
|
5
|
+
#
|
|
6
|
+
# WINDOWS
|
|
7
|
+
# =======
|
|
8
|
+
# Windows is supported, but must use URLs in the modern structure like:
|
|
9
|
+
# file:///x:/path/to/the/file.png
|
|
10
|
+
# or as a relative path:
|
|
11
|
+
# directory/file.png
|
|
12
|
+
# or as an absolute path from the current drive:
|
|
13
|
+
# /path/to/the/file.png
|
|
14
|
+
#
|
|
15
|
+
# Ruby's URI parser does not like backslashes, nor can it handle filenames as URLs starting
|
|
16
|
+
# with a drive letter as it thinks you're giving it a scheme.
|
|
17
|
+
#
|
|
18
|
+
# URL ENCODING
|
|
19
|
+
# ============
|
|
20
|
+
# This module assumes the URL that is passed in has been URL-encoded. If for some reason
|
|
21
|
+
# you're passing in a filename that hasn't been taken from an XML document's attribute,
|
|
22
|
+
# you will want to URL encode it before you pass it in.
|
|
23
|
+
#
|
|
24
|
+
# FILES READ AS BINARY
|
|
25
|
+
# ====================
|
|
26
|
+
# At the moment, prawn-svg uses this class only to load graphical files, which are binary.
|
|
27
|
+
# This class therefore uses File.binread to read file data. If it is ever used in the future
|
|
28
|
+
# to load text files, it will have to be taught about what kind of file it's expecting to
|
|
29
|
+
# read, and adjust the file read function accordingly.
|
|
30
|
+
#
|
|
31
|
+
module Prawn::SVG::Loaders
|
|
32
|
+
class File
|
|
33
|
+
attr_reader :root_path
|
|
34
|
+
|
|
35
|
+
def initialize(root_path)
|
|
36
|
+
if root_path.empty?
|
|
37
|
+
raise ArgumentError,
|
|
38
|
+
"An empty string is not a valid root path. Use '.' if you want the current working directory."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@root_path = ::File.expand_path(root_path)
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, "#{root_path} is not a directory" unless Dir.exist?(@root_path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def from_url(url)
|
|
47
|
+
uri = build_uri(url)
|
|
48
|
+
|
|
49
|
+
if uri && uri.scheme.nil? && uri.path
|
|
50
|
+
load_file(uri.path)
|
|
51
|
+
|
|
52
|
+
elsif uri && uri.scheme == 'file'
|
|
53
|
+
assert_valid_file_uri!(uri)
|
|
54
|
+
path = windows? ? fix_windows_path(uri.path) : uri.path
|
|
55
|
+
load_file(path)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def load_file(path)
|
|
62
|
+
path = Addressable::URI.unencode(path)
|
|
63
|
+
path = build_absolute_and_expand_path(path)
|
|
64
|
+
assert_valid_path!(path)
|
|
65
|
+
assert_file_exists!(path)
|
|
66
|
+
::File.binread(path)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_uri(url)
|
|
70
|
+
URI(url)
|
|
71
|
+
rescue URI::InvalidURIError
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def assert_valid_path!(path)
|
|
75
|
+
# TODO : case sensitive comparison, but it's going to be a bit of a headache
|
|
76
|
+
# making it dependent on whether the file system is case sensitive or not.
|
|
77
|
+
# Leaving it like this until it's a problem for someone.
|
|
78
|
+
|
|
79
|
+
unless path.start_with?("#{root_path}#{::File::SEPARATOR}")
|
|
80
|
+
raise Prawn::SVG::UrlLoader::Error, "file path is not inside the root path of #{root_path}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_absolute_and_expand_path(path)
|
|
85
|
+
::File.expand_path(path, root_path)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def assert_valid_file_uri!(uri)
|
|
89
|
+
unless uri.host.nil? || uri.host.empty?
|
|
90
|
+
raise Prawn::SVG::UrlLoader::Error,
|
|
91
|
+
"prawn-svg does not suport file: URLs with a host. Your URL probably doesn't start with three slashes, and it should."
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def assert_file_exists!(path)
|
|
96
|
+
raise Prawn::SVG::UrlLoader::Error, "File #{path} does not exist" unless ::File.exist?(path)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def fix_windows_path(path)
|
|
100
|
+
if path.match(%r{\A/[a-z]:/}i)
|
|
101
|
+
path[1..]
|
|
102
|
+
else
|
|
103
|
+
path
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def windows?
|
|
108
|
+
!!(RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
|
|
3
|
+
module Prawn::SVG::Loaders
|
|
4
|
+
class Web
|
|
5
|
+
def from_url(url)
|
|
6
|
+
uri = build_uri(url)
|
|
7
|
+
|
|
8
|
+
perform_request(uri) if uri && %w[http https].include?(uri.scheme)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def build_uri(url)
|
|
14
|
+
URI(url)
|
|
15
|
+
rescue URI::InvalidURIError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def perform_request(uri)
|
|
19
|
+
Net::HTTP.get(uri)
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
raise Prawn::SVG::UrlLoader::Error, e.message
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|