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.
Files changed (160) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +339 -0
  4. data/GPLv3 +674 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-prawn-svg +21 -0
  7. data/README.md +107 -0
  8. data/data/fonts/Courier-Bold.afm +342 -0
  9. data/data/fonts/Courier-BoldOblique.afm +342 -0
  10. data/data/fonts/Courier-Oblique.afm +342 -0
  11. data/data/fonts/Courier.afm +342 -0
  12. data/data/fonts/Helvetica-Bold.afm +2827 -0
  13. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  14. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  15. data/data/fonts/Helvetica.afm +3051 -0
  16. data/data/fonts/MustRead.html +19 -0
  17. data/data/fonts/Symbol.afm +213 -0
  18. data/data/fonts/Times-Bold.afm +2588 -0
  19. data/data/fonts/Times-BoldItalic.afm +2384 -0
  20. data/data/fonts/Times-Italic.afm +2667 -0
  21. data/data/fonts/Times-Roman.afm +2419 -0
  22. data/data/fonts/ZapfDingbats.afm +225 -0
  23. data/lib/pdf/core.rb +4 -0
  24. data/lib/prawn/measurement_extensions.rb +4 -0
  25. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  26. data/lib/prawn/svg/attributes/mask.rb +16 -0
  27. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  28. data/lib/prawn/svg/attributes/space.rb +10 -0
  29. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  30. data/lib/prawn/svg/attributes/transform.rb +10 -0
  31. data/lib/prawn/svg/attributes.rb +6 -0
  32. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  33. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  34. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  35. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  36. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  37. data/lib/prawn/svg/color.rb +240 -0
  38. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  39. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  40. data/lib/prawn/svg/css/font_parser.rb +46 -0
  41. data/lib/prawn/svg/css/selector_parser.rb +178 -0
  42. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  43. data/lib/prawn/svg/css/values_parser.rb +68 -0
  44. data/lib/prawn/svg/document.rb +170 -0
  45. data/lib/prawn/svg/elements/anchor.rb +10 -0
  46. data/lib/prawn/svg/elements/base.rb +312 -0
  47. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  48. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  49. data/lib/prawn/svg/elements/circle.rb +25 -0
  50. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  51. data/lib/prawn/svg/elements/container.rb +10 -0
  52. data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
  53. data/lib/prawn/svg/elements/ellipse.rb +22 -0
  54. data/lib/prawn/svg/elements/gradient.rb +184 -0
  55. data/lib/prawn/svg/elements/ignored.rb +5 -0
  56. data/lib/prawn/svg/elements/image.rb +177 -0
  57. data/lib/prawn/svg/elements/line.rb +27 -0
  58. data/lib/prawn/svg/elements/marker.rb +72 -0
  59. data/lib/prawn/svg/elements/mask.rb +65 -0
  60. data/lib/prawn/svg/elements/path.rb +294 -0
  61. data/lib/prawn/svg/elements/pattern.rb +150 -0
  62. data/lib/prawn/svg/elements/polygon.rb +25 -0
  63. data/lib/prawn/svg/elements/polyline.rb +23 -0
  64. data/lib/prawn/svg/elements/rect.rb +64 -0
  65. data/lib/prawn/svg/elements/root.rb +22 -0
  66. data/lib/prawn/svg/elements/switch.rb +92 -0
  67. data/lib/prawn/svg/elements/text.rb +99 -0
  68. data/lib/prawn/svg/elements/text_component.rb +346 -0
  69. data/lib/prawn/svg/elements/text_node.rb +308 -0
  70. data/lib/prawn/svg/elements/text_path.rb +153 -0
  71. data/lib/prawn/svg/elements/use.rb +142 -0
  72. data/lib/prawn/svg/elements/viewport.rb +29 -0
  73. data/lib/prawn/svg/elements.rb +46 -0
  74. data/lib/prawn/svg/extension.rb +25 -0
  75. data/lib/prawn/svg/font.rb +20 -0
  76. data/lib/prawn/svg/font_metrics.rb +144 -0
  77. data/lib/prawn/svg/font_registry.rb +315 -0
  78. data/lib/prawn/svg/funciri.rb +14 -0
  79. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  80. data/lib/prawn/svg/gradients.rb +53 -0
  81. data/lib/prawn/svg/interface.rb +72 -0
  82. data/lib/prawn/svg/length.rb +43 -0
  83. data/lib/prawn/svg/link_renderer.rb +40 -0
  84. data/lib/prawn/svg/loaders/data.rb +17 -0
  85. data/lib/prawn/svg/loaders/file.rb +111 -0
  86. data/lib/prawn/svg/loaders/web.rb +24 -0
  87. data/lib/prawn/svg/paint.rb +67 -0
  88. data/lib/prawn/svg/pathable.rb +153 -0
  89. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  90. data/lib/prawn/svg/pdfmatrix.rb +37 -0
  91. data/lib/prawn/svg/percentage.rb +24 -0
  92. data/lib/prawn/svg/properties.rb +329 -0
  93. data/lib/prawn/svg/renderer.rb +249 -0
  94. data/lib/prawn/svg/state.rb +24 -0
  95. data/lib/prawn/svg/transform_parser.rb +78 -0
  96. data/lib/prawn/svg/ttc.rb +35 -0
  97. data/lib/prawn/svg/ttf.rb +95 -0
  98. data/lib/prawn/svg/url_loader.rb +46 -0
  99. data/lib/prawn/svg/version.rb +5 -0
  100. data/lib/prawn/table.rb +4 -0
  101. data/lib/prawn-svg.rb +53 -0
  102. data/lib/prawn.rb +10 -0
  103. data/lib/prawnx/annotations.rb +21 -0
  104. data/lib/prawnx/document/bounding_box.rb +193 -0
  105. data/lib/prawnx/document/column_box.rb +91 -0
  106. data/lib/prawnx/document/internals.rb +144 -0
  107. data/lib/prawnx/document/span.rb +25 -0
  108. data/lib/prawnx/document.rb +253 -0
  109. data/lib/prawnx/encoding.rb +29 -0
  110. data/lib/prawnx/errors.rb +37 -0
  111. data/lib/prawnx/font.rb +214 -0
  112. data/lib/prawnx/fonts/afm.rb +171 -0
  113. data/lib/prawnx/fonts/otf.rb +50 -0
  114. data/lib/prawnx/fonts/ttf.rb +410 -0
  115. data/lib/prawnx/graphic_state.rb +41 -0
  116. data/lib/prawnx/graphics/color.rb +148 -0
  117. data/lib/prawnx/graphics/patterns.rb +100 -0
  118. data/lib/prawnx/graphics/style.rb +171 -0
  119. data/lib/prawnx/graphics.rb +208 -0
  120. data/lib/prawnx/grid.rb +216 -0
  121. data/lib/prawnx/images/image.rb +244 -0
  122. data/lib/prawnx/images/png_decoder.rb +65 -0
  123. data/lib/prawnx/images.rb +115 -0
  124. data/lib/prawnx/measurement_extensions.rb +40 -0
  125. data/lib/prawnx/measurements.rb +62 -0
  126. data/lib/prawnx/outline.rb +146 -0
  127. data/lib/prawnx/page.rb +125 -0
  128. data/lib/prawnx/page_geometry.rb +27 -0
  129. data/lib/prawnx/pdf/outline_node.rb +53 -0
  130. data/lib/prawnx/pdf/reference.rb +31 -0
  131. data/lib/prawnx/pdf/writer.rb +105 -0
  132. data/lib/prawnx/pdf.rb +66 -0
  133. data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
  134. data/lib/prawnx/pdf_a.rb +130 -0
  135. data/lib/prawnx/pdf_core.rb +25 -0
  136. data/lib/prawnx/repeater.rb +79 -0
  137. data/lib/prawnx/security.rb +108 -0
  138. data/lib/prawnx/stamp.rb +74 -0
  139. data/lib/prawnx/table/cell/kinds.rb +246 -0
  140. data/lib/prawnx/table/cell.rb +331 -0
  141. data/lib/prawnx/table/cells.rb +208 -0
  142. data/lib/prawnx/table.rb +296 -0
  143. data/lib/prawnx/text/box.rb +16 -0
  144. data/lib/prawnx/text/formatted/arranger.rb +217 -0
  145. data/lib/prawnx/text/formatted/box.rb +312 -0
  146. data/lib/prawnx/text/formatted/fragment.rb +205 -0
  147. data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
  148. data/lib/prawnx/text/formatted/parser.rb +149 -0
  149. data/lib/prawnx/text/formatted/wrap.rb +103 -0
  150. data/lib/prawnx/text.rb +281 -0
  151. data/lib/prawnx/transformation_stack.rb +43 -0
  152. data/lib/prawnx/true_type/cff_subset.rb +157 -0
  153. data/lib/prawnx/true_type/file.rb +371 -0
  154. data/lib/prawnx/true_type/open_type_layout.rb +189 -0
  155. data/lib/prawnx/true_type/simple_subsets.rb +94 -0
  156. data/lib/prawnx/true_type/subset.rb +187 -0
  157. data/lib/prawnx/version.rb +7 -0
  158. data/lib/prawnx/view.rb +28 -0
  159. data/lib/prawnx.rb +102 -0
  160. 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