giga-fast-kit 0.0.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 (75) hide show
  1. checksums.yaml +7 -0
  2. data/giga-fast-kit.gemspec +12 -0
  3. data/prawn-2.5.0/COPYING +2 -0
  4. data/prawn-2.5.0/GPLv2 +339 -0
  5. data/prawn-2.5.0/GPLv3 +674 -0
  6. data/prawn-2.5.0/LICENSE +56 -0
  7. data/prawn-2.5.0/data/fonts/Courier-Bold.afm +342 -0
  8. data/prawn-2.5.0/data/fonts/Courier-BoldOblique.afm +342 -0
  9. data/prawn-2.5.0/data/fonts/Courier-Oblique.afm +342 -0
  10. data/prawn-2.5.0/data/fonts/Courier.afm +342 -0
  11. data/prawn-2.5.0/data/fonts/Helvetica-Bold.afm +2827 -0
  12. data/prawn-2.5.0/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  13. data/prawn-2.5.0/data/fonts/Helvetica-Oblique.afm +3051 -0
  14. data/prawn-2.5.0/data/fonts/Helvetica.afm +3051 -0
  15. data/prawn-2.5.0/data/fonts/MustRead.html +19 -0
  16. data/prawn-2.5.0/data/fonts/Symbol.afm +213 -0
  17. data/prawn-2.5.0/data/fonts/Times-Bold.afm +2588 -0
  18. data/prawn-2.5.0/data/fonts/Times-BoldItalic.afm +2384 -0
  19. data/prawn-2.5.0/data/fonts/Times-Italic.afm +2667 -0
  20. data/prawn-2.5.0/data/fonts/Times-Roman.afm +2419 -0
  21. data/prawn-2.5.0/data/fonts/ZapfDingbats.afm +225 -0
  22. data/prawn-2.5.0/lib/prawn/document/bounding_box.rb +625 -0
  23. data/prawn-2.5.0/lib/prawn/document/column_box.rb +177 -0
  24. data/prawn-2.5.0/lib/prawn/document/internals.rb +76 -0
  25. data/prawn-2.5.0/lib/prawn/document/span.rb +65 -0
  26. data/prawn-2.5.0/lib/prawn/document.rb +846 -0
  27. data/prawn-2.5.0/lib/prawn/encoding.rb +85 -0
  28. data/prawn-2.5.0/lib/prawn/errors.rb +73 -0
  29. data/prawn-2.5.0/lib/prawn/font.rb +567 -0
  30. data/prawn-2.5.0/lib/prawn/font_metric_cache.rb +50 -0
  31. data/prawn-2.5.0/lib/prawn/fonts/afm.rb +332 -0
  32. data/prawn-2.5.0/lib/prawn/fonts/dfont.rb +42 -0
  33. data/prawn-2.5.0/lib/prawn/fonts/otf.rb +14 -0
  34. data/prawn-2.5.0/lib/prawn/fonts/to_unicode_cmap.rb +151 -0
  35. data/prawn-2.5.0/lib/prawn/fonts/ttc.rb +41 -0
  36. data/prawn-2.5.0/lib/prawn/fonts/ttf.rb +615 -0
  37. data/prawn-2.5.0/lib/prawn/fonts.rb +14 -0
  38. data/prawn-2.5.0/lib/prawn/graphics/blend_mode.rb +62 -0
  39. data/prawn-2.5.0/lib/prawn/graphics/cap_style.rb +44 -0
  40. data/prawn-2.5.0/lib/prawn/graphics/color.rb +257 -0
  41. data/prawn-2.5.0/lib/prawn/graphics/dash.rb +123 -0
  42. data/prawn-2.5.0/lib/prawn/graphics/join_style.rb +60 -0
  43. data/prawn-2.5.0/lib/prawn/graphics/patterns.rb +389 -0
  44. data/prawn-2.5.0/lib/prawn/graphics/transformation.rb +172 -0
  45. data/prawn-2.5.0/lib/prawn/graphics/transparency.rb +79 -0
  46. data/prawn-2.5.0/lib/prawn/graphics.rb +791 -0
  47. data/prawn-2.5.0/lib/prawn/grid.rb +449 -0
  48. data/prawn-2.5.0/lib/prawn/image_handler.rb +61 -0
  49. data/prawn-2.5.0/lib/prawn/images/image.rb +48 -0
  50. data/prawn-2.5.0/lib/prawn/images/jpg.rb +121 -0
  51. data/prawn-2.5.0/lib/prawn/images/png.rb +383 -0
  52. data/prawn-2.5.0/lib/prawn/images.rb +195 -0
  53. data/prawn-2.5.0/lib/prawn/measurement_extensions.rb +82 -0
  54. data/prawn-2.5.0/lib/prawn/measurements.rb +129 -0
  55. data/prawn-2.5.0/lib/prawn/outline.rb +316 -0
  56. data/prawn-2.5.0/lib/prawn/repeater.rb +140 -0
  57. data/prawn-2.5.0/lib/prawn/security/arcfour.rb +54 -0
  58. data/prawn-2.5.0/lib/prawn/security.rb +308 -0
  59. data/prawn-2.5.0/lib/prawn/soft_mask.rb +105 -0
  60. data/prawn-2.5.0/lib/prawn/stamp.rb +147 -0
  61. data/prawn-2.5.0/lib/prawn/text/box.rb +192 -0
  62. data/prawn-2.5.0/lib/prawn/text/formatted/arranger.rb +396 -0
  63. data/prawn-2.5.0/lib/prawn/text/formatted/box.rb +725 -0
  64. data/prawn-2.5.0/lib/prawn/text/formatted/fragment.rb +373 -0
  65. data/prawn-2.5.0/lib/prawn/text/formatted/line_wrap.rb +335 -0
  66. data/prawn-2.5.0/lib/prawn/text/formatted/parser.rb +284 -0
  67. data/prawn-2.5.0/lib/prawn/text/formatted/wrap.rb +166 -0
  68. data/prawn-2.5.0/lib/prawn/text/formatted.rb +82 -0
  69. data/prawn-2.5.0/lib/prawn/text.rb +701 -0
  70. data/prawn-2.5.0/lib/prawn/transformation_stack.rb +63 -0
  71. data/prawn-2.5.0/lib/prawn/utilities.rb +34 -0
  72. data/prawn-2.5.0/lib/prawn/version.rb +6 -0
  73. data/prawn-2.5.0/lib/prawn/view.rb +116 -0
  74. data/prawn-2.5.0/lib/prawn.rb +83 -0
  75. metadata +114 -0
@@ -0,0 +1,308 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/md5'
4
+
5
+ require_relative 'security/arcfour'
6
+
7
+ module Prawn
8
+ class Document
9
+ # Implements PDF encryption (password protection and permissions) as
10
+ # specified in the PDF Reference, version 1.3, section 3.5 "Encryption".
11
+ module Security
12
+ # @group Experimental API
13
+
14
+ # Encrypts the document, to protect confidential data or control
15
+ # modifications to the document. The encryption algorithm used is detailed
16
+ # in the PDF Reference 1.3, section 3.5 "Encryption", and it is
17
+ # implemented by all major PDF readers.
18
+ #
19
+ # #### Examples
20
+ #
21
+ # Deny printing to everyone, but allow anyone to open without a password:
22
+ #
23
+ # ```ruby
24
+ # encrypt_document permissions: { print_document: false },
25
+ # owner_password: :random
26
+ # ```
27
+ #
28
+ # Set a user and owner password on the document, with full permissions for
29
+ # both the user and the owner:
30
+ #
31
+ # ```ruby
32
+ # encrypt_document user_password: 'foo', owner_password: 'bar'
33
+ # ```
34
+ #
35
+ # Set no passwords, grant all permissions (This is useful because the
36
+ # default in some readers, if no permissions are specified, is "deny"):
37
+ #
38
+ # ```ruby
39
+ # encrypt_document
40
+ # ```
41
+ #
42
+ # #### Caveats
43
+ #
44
+ # * The encryption used is weak; the key is password-derived and is
45
+ # limited to 40 bits, due to US export controls in effect at the time
46
+ # the PDF standard was written.
47
+ # * There is nothing technologically requiring PDF readers to respect the
48
+ # permissions embedded in a document. Many PDF readers do not.
49
+ # * In short, you have **no security at all** against a moderately
50
+ # motivated person. Don't use this for anything super-serious. This is
51
+ # not a limitation of Prawn, but is rather a built-in limitation of the
52
+ # PDF format.
53
+ #
54
+ # @param options [Hash{Symbol => any}]
55
+ # @option options :user_password [String]
56
+ # Password required to open the document. If this is omitted or empty,
57
+ # no password will be required. The document will still be encrypted,
58
+ # but anyone can read it.
59
+ # @option options :owner_password [String, :random]
60
+ # Password required to make modifications to the document or change or
61
+ # override its permissions. If this is set to `:random`, a random
62
+ # password will be used; this can be useful if you never want users to
63
+ # be able to override the document permissions.
64
+ # @option options :permissions [Hash{Symbol => Boolean}]
65
+ # A hash mapping permission symbols (see below) to `true` or `false`.
66
+ # `true` means "permitted", and `false` means "not permitted". All
67
+ # permissions default to `true`.
68
+ #
69
+ # The following permissions can be specified:
70
+ #
71
+ # - `:print_document` -- Print document.
72
+ # - `:modify_contents` -- Modify contents of document (other than text
73
+ # annotations and interactive form fields).
74
+ # - `:copy_contents` -- Copy text and graphics from document.
75
+ # - `:modify_annotations` -- Add or modify text annotations and
76
+ # interactive form fields.
77
+ # @return [void]
78
+ def encrypt_document(options = {})
79
+ Prawn.verify_options(%i[user_password owner_password permissions], options)
80
+ @user_password = options.delete(:user_password) || ''
81
+
82
+ @owner_password = options.delete(:owner_password) || @user_password
83
+ if @owner_password == :random
84
+ # Generate a completely ridiculous password
85
+ @owner_password = (1..32).map { rand(256) }.pack('c*')
86
+ end
87
+
88
+ self.permissions = options.delete(:permissions) || {}
89
+
90
+ # Shove the necessary entries in the trailer and enable encryption.
91
+ state.trailer[:Encrypt] = encryption_dictionary
92
+ state.encrypt = true
93
+ state.encryption_key = user_encryption_key
94
+ end
95
+
96
+ # Encrypts the given string under the given key, also requiring the object
97
+ # ID and generation number of the reference.
98
+ #
99
+ # See Algorithm 3.1.
100
+ #
101
+ # @param str [String]
102
+ # @param key [String]
103
+ # @param id [Integer]
104
+ # @param gen [Integer]
105
+ # @return [String]
106
+ def self.encrypt_string(str, key, id, gen)
107
+ # Convert ID and Gen number into little-endian truncated byte strings
108
+ id = [id].pack('V')[0, 3]
109
+ gen = [gen].pack('V')[0, 2]
110
+ extended_key = "#{key}#{id}#{gen}"
111
+
112
+ # Compute the RC4 key from the extended key and perform the encryption
113
+ rc4_key = Digest::MD5.digest(extended_key)[0, 10]
114
+ Arcfour.new(rc4_key).encrypt(str)
115
+ end
116
+
117
+ private
118
+
119
+ # Provides the values for the trailer encryption dictionary.
120
+ def encryption_dictionary
121
+ {
122
+ Filter: :Standard, # default PDF security handler
123
+ V: 1, # "Algorithm 3.1", PDF reference 1.3
124
+ R: 2, # Revision 2 of the algorithm
125
+ O: PDF::Core::ByteString.new(owner_password_hash),
126
+ U: PDF::Core::ByteString.new(user_password_hash),
127
+ P: permissions_value,
128
+ }
129
+ end
130
+
131
+ # Flags in the permissions word, numbered as LSB = 1
132
+ PERMISSIONS_BITS = {
133
+ print_document: 3,
134
+ modify_contents: 4,
135
+ copy_contents: 5,
136
+ modify_annotations: 6,
137
+ }.freeze
138
+ private_constant :PERMISSIONS_BITS
139
+
140
+ FULL_PERMISSIONS = 0b1111_1111_1111_1111_1111_1111_1111_1111
141
+ private_constant :FULL_PERMISSIONS
142
+
143
+ def permissions=(perms = {})
144
+ @permissions ||= FULL_PERMISSIONS
145
+ perms.each do |key, value|
146
+ unless PERMISSIONS_BITS[key]
147
+ raise(
148
+ ArgumentError,
149
+ "Unknown permission :#{key}. Valid options: " +
150
+ PERMISSIONS_BITS.keys.map(&:inspect).join(', '),
151
+ )
152
+ end
153
+
154
+ # 0-based bit number, from LSB
155
+ bit_position = PERMISSIONS_BITS[key] - 1
156
+
157
+ if value # set bit
158
+ @permissions |= (1 << bit_position)
159
+ else # clear bit
160
+ @permissions &= ~(1 << bit_position)
161
+ end
162
+ end
163
+ end
164
+
165
+ def permissions_value
166
+ @permissions || FULL_PERMISSIONS
167
+ end
168
+
169
+ PASSWORD_PADDING =
170
+ '28BF4E5E4E758A4164004E56FFFA01082E2E00B6D0683E802F0CA9FE6453697A'
171
+ .scan(/../).map { |x| x.to_i(16) }.pack('c*')
172
+
173
+ # Pads or truncates a password to 32 bytes as per Alg 3.2.
174
+ def pad_password(password)
175
+ password = password[0, 32]
176
+ password + PASSWORD_PADDING[0, 32 - password.length]
177
+ end
178
+
179
+ def user_encryption_key
180
+ @user_encryption_key ||=
181
+ begin
182
+ md5 = Digest::MD5.new
183
+ md5 << pad_password(@user_password)
184
+ md5 << owner_password_hash
185
+ md5 << [permissions_value].pack('V')
186
+ md5.digest[0, 5]
187
+ end
188
+ end
189
+
190
+ # The O (owner) value in the encryption dictionary. Algorithm 3.3.
191
+ def owner_password_hash
192
+ @owner_password_hash ||=
193
+ begin
194
+ key = Digest::MD5.digest(pad_password(@owner_password))[0, 5]
195
+ Arcfour.new(key).encrypt(pad_password(@user_password))
196
+ end
197
+ end
198
+
199
+ # The U (user) value in the encryption dictionary. Algorithm 3.4.
200
+ def user_password_hash
201
+ Arcfour.new(user_encryption_key).encrypt(PASSWORD_PADDING)
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+ # @private
208
+ module PDF
209
+ module Core # rubocop: disable Style/Documentation
210
+ module_function
211
+
212
+ # Like pdf_object, but returns an encrypted result if required.
213
+ # For direct objects, requires the object identifier and generation number
214
+ # from the indirect object referencing obj.
215
+ #
216
+ # @private
217
+ def encrypted_pdf_object(obj, key, id, gen, in_content_stream = false)
218
+ case obj
219
+ when Array
220
+ array_content = obj.map { |e|
221
+ encrypted_pdf_object(e, key, id, gen, in_content_stream)
222
+ }.join(' ')
223
+ "[#{array_content}]"
224
+ when LiteralString
225
+ obj =
226
+ ByteString.new(
227
+ Prawn::Document::Security.encrypt_string(obj, key, id, gen),
228
+ ).gsub(/[\\\r()]/, STRING_ESCAPE_MAP)
229
+ "(#{obj})"
230
+ when Time
231
+ obj = "#{obj.strftime('D:%Y%m%d%H%M%S%z').chop.chop}'00'"
232
+ obj =
233
+ ByteString.new(
234
+ Prawn::Document::Security.encrypt_string(obj, key, id, gen),
235
+ ).gsub(/[\\\r()]/, STRING_ESCAPE_MAP)
236
+ "(#{obj})"
237
+ when String
238
+ pdf_object(
239
+ ByteString.new(
240
+ Prawn::Document::Security.encrypt_string(obj, key, id, gen),
241
+ ),
242
+ in_content_stream,
243
+ )
244
+ when ::Hash
245
+ hash_content = obj.map { |k, v|
246
+ unless k.is_a?(String) || k.is_a?(Symbol)
247
+ raise PDF::Core::Errors::FailedObjectConversion,
248
+ 'A PDF Dictionary must be keyed by names'
249
+ end
250
+ "#{pdf_object(k.to_sym, in_content_stream)} #{encrypted_pdf_object(v, key, id, gen, in_content_stream)}\n"
251
+ }.join('')
252
+ "<< #{hash_content}>>"
253
+ when NameTree::Value
254
+ "#{pdf_object(obj.name)} #{encrypted_pdf_object(obj.value, key, id, gen, in_content_stream)}"
255
+ when PDF::Core::OutlineRoot, PDF::Core::OutlineItem
256
+ encrypted_pdf_object(obj.to_hash, key, id, gen, in_content_stream)
257
+ else # delegate back to pdf_object
258
+ pdf_object(obj, in_content_stream)
259
+ end
260
+ end
261
+
262
+ # @private
263
+ class Stream
264
+ # Encrypt stream.
265
+ #
266
+ # @param key [String]
267
+ # @param id [Integer]
268
+ # @param gen [Integer]
269
+ # @return [String]
270
+ def encrypted_object(key, id, gen)
271
+ if filtered_stream
272
+ "stream\n#{
273
+ Prawn::Document::Security.encrypt_string(
274
+ filtered_stream, key, id, gen,
275
+ )
276
+ }\nendstream\n"
277
+ else
278
+ ''
279
+ end
280
+ end
281
+ end
282
+
283
+ # @private
284
+ class Reference
285
+ # Returns the object definition for the object this references, keyed from
286
+ # `key`.
287
+ #
288
+ # @param key [String]
289
+ # @return [String]
290
+ def encrypted_object(key)
291
+ @on_encode&.call(self)
292
+
293
+ output = +"#{@identifier} #{gen} obj\n"
294
+ if @stream.empty?
295
+ output <<
296
+ PDF::Core.encrypted_pdf_object(data, key, @identifier, gen) << "\n"
297
+ else
298
+ output << PDF::Core.encrypted_pdf_object(
299
+ data.merge(@stream.data), key, @identifier, gen,
300
+ ) << "\n" <<
301
+ @stream.encrypted_object(key, @identifier, gen)
302
+ end
303
+
304
+ output << "endobj\n"
305
+ end
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ # This module is used to create arbitrary transparency in document. Using
5
+ # a soft mask allows creating more visually rich documents.
6
+ module SoftMask
7
+ # @group Stable API
8
+
9
+ # Apply soft mask.
10
+ #
11
+ # You must group soft mask and graphics it's applied to under
12
+ # `save_graphics_state` because soft mask is a part of graphic state in PDF.
13
+ #
14
+ # Note that soft mask is applied only to the following content in the
15
+ # graphic state. Anything that comes before `soft_mask` is drawn without
16
+ # mask.
17
+ #
18
+ # Conceptually, soft mask is an alpha channel. Luminosity of the drawing in
19
+ # the soft mask defines the transparency of the drawing the mask is applied
20
+ # to. 0.0 mask luminosity ("black") results in a fully opaque target image and
21
+ # 1.0 mask luminosity ("white") results in a fully transparent target image.
22
+ # Grey values result in some semi-transparent target image.
23
+ #
24
+ # Note: you can use color in mask drawings but it makes harder to reason
25
+ # about the resulting value of alpha channel as it requires an additional
26
+ # luminosity calculation. However, this also allows achieving some advanced
27
+ # artistic effects (e.g. full-color photos in masks to get an effect similar
28
+ # to double exposure).
29
+ #
30
+ # @example
31
+ # pdf.save_graphics_state do
32
+ # pdf.soft_mask do
33
+ # pdf.fill_color "444444"
34
+ # pdf.fill_polygon [0, 40], [60, 10], [120, 40], [60, 68]
35
+ # end
36
+ # pdf.fill_color '000000'
37
+ # pdf.fill_rectangle [0, 50], 120, 68
38
+ # end
39
+ #
40
+ # @yield Mask content.
41
+ # @return [void]
42
+ def soft_mask(&block)
43
+ renderer.min_version(1.4)
44
+
45
+ group_attrs = ref!(
46
+ Type: :Group,
47
+ S: :Transparency,
48
+ CS: :DeviceRGB,
49
+ I: false,
50
+ K: false,
51
+ )
52
+
53
+ group = ref!(
54
+ Type: :XObject,
55
+ Subtype: :Form,
56
+ BBox: state.page.dimensions,
57
+ Group: group_attrs,
58
+ )
59
+
60
+ state.page.stamp_stream(group, &block)
61
+
62
+ mask = ref!(
63
+ Type: :Mask,
64
+ S: :Luminosity,
65
+ G: group,
66
+ )
67
+
68
+ g_state = ref!(
69
+ Type: :ExtGState,
70
+ SMask: mask,
71
+
72
+ AIS: false,
73
+ BM: :Normal,
74
+ OP: false,
75
+ op: false,
76
+ OPM: 1,
77
+ SA: true,
78
+ )
79
+
80
+ registry_key = {
81
+ bbox: state.page.dimensions,
82
+ mask: [group.stream.filters.normalized, group.stream.filtered_stream],
83
+ page: state.page_count,
84
+ }.hash
85
+
86
+ if soft_mask_registry[registry_key]
87
+ renderer.add_content("/#{soft_mask_registry[registry_key]} gs")
88
+ else
89
+ masks = page.resources[:ExtGState] ||= {}
90
+ id = masks.empty? ? 'GS1' : masks.keys.max.succ
91
+ masks[id] = g_state
92
+
93
+ soft_mask_registry[registry_key] = id
94
+
95
+ renderer.add_content("/#{id} gs")
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def soft_mask_registry
102
+ @soft_mask_registry ||= {}
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ # This module is used to create content that will be included multiple times
5
+ # in a document. Using a stamp has three advantages over creating content anew
6
+ # each time it is placed on the page:
7
+ # * Faster document creation.
8
+ # * Smaller final document.
9
+ # * Faster display on subsequent displays of the repeated element because the
10
+ # viewer application can cache the rendered results.
11
+ #
12
+ # @example
13
+ # pdf.create_stamp("my_stamp") {
14
+ # pdf.fill_circle([10, 15], 5)
15
+ # pdf.draw_text("hello world", at: [20, 10])
16
+ # }
17
+ # pdf.stamp("my_stamp")
18
+ module Stamp
19
+ # @group Stable API
20
+
21
+ # Renders the stamp.
22
+ #
23
+ # @example
24
+ # pdf.create_stamp("my_stamp") {
25
+ # pdf.fill_circle([10, 15], 5)
26
+ # pdf.text("hello world", at: [20, 10])
27
+ # }
28
+ # pdf.stamp("my_stamp")
29
+ #
30
+ # @param name [String]
31
+ # @return [void]
32
+ # @raise [Prawn::Errors::InvalidName] if name is empty.
33
+ # @raise [Prawn::Errors::UndefinedObjectName] if no stamp has been created
34
+ # with this name.
35
+ def stamp(name)
36
+ dictionary_name, dictionary = stamp_dictionary(name)
37
+ renderer.add_content("/#{dictionary_name} Do")
38
+ update_annotation_references(dictionary.data[:Annots])
39
+ state.page.xobjects.merge!(dictionary_name => dictionary)
40
+ end
41
+
42
+ # Renders the stamp at a position offset from the initial coords at which
43
+ # the elements of the stamp was created.
44
+ #
45
+ # @example
46
+ # pdf.create_stamp("circle") do
47
+ # pdf.fill_circle([0, 0], 25)
48
+ # end
49
+ # # draws a circle at 100, 100
50
+ # pdf.stamp_at("circle", [100, 100])
51
+ #
52
+ # @param name [String]
53
+ # @param point [Array(Number, Number)]
54
+ # @return [void]
55
+ # @see [stamp] for exceptions that might be raised.
56
+ def stamp_at(name, point)
57
+ translate(point[0], point[1]) { stamp(name) }
58
+ end
59
+
60
+ # Creates a re-usable stamp.
61
+ #
62
+ # @example
63
+ # pdf.create_stamp("my_stamp") {
64
+ # pdf.fill_circle([10, 15], 5)
65
+ # pdf.draw_text("hello world", at: [20, 10])
66
+ # }
67
+ #
68
+ # @param name [String] Stamp name.
69
+ # @yield Stamp content.
70
+ # @return [void]
71
+ # @raise [Prawn::Errors::NameTaken]
72
+ # if a stamp already exists in this document with this name.
73
+ # @raise [Prawn::Errors::InvalidName] if name is empty.
74
+ def create_stamp(name, &block)
75
+ dictionary = create_stamp_dictionary(name)
76
+
77
+ state.page.stamp_stream(dictionary, &block)
78
+ end
79
+
80
+ private
81
+
82
+ def stamp_dictionary_registry
83
+ @stamp_dictionary_registry ||= {}
84
+ end
85
+
86
+ def next_stamp_dictionary_id
87
+ stamp_dictionary_registry.length + 1
88
+ end
89
+
90
+ def stamp_dictionary(name)
91
+ raise Prawn::Errors::InvalidName if name.empty?
92
+ if stamp_dictionary_registry[name].nil?
93
+ raise Prawn::Errors::UndefinedObjectName
94
+ end
95
+
96
+ dict = stamp_dictionary_registry[name]
97
+
98
+ dictionary_name = dict[:stamp_dictionary_name]
99
+ dictionary = dict[:stamp_dictionary]
100
+ [dictionary_name, dictionary]
101
+ end
102
+
103
+ def create_stamp_dictionary(name)
104
+ raise Prawn::Errors::InvalidName if name.empty?
105
+ raise Prawn::Errors::NameTaken unless stamp_dictionary_registry[name].nil?
106
+
107
+ # BBox origin is the lower left margin of the page, so we need
108
+ # it to be the full dimension of the page, or else things that
109
+ # should appear near the top or right margin are invisible
110
+ dictionary = ref!(
111
+ Type: :XObject,
112
+ Subtype: :Form,
113
+ BBox: [
114
+ 0, 0,
115
+ state.page.dimensions[2], state.page.dimensions[3],
116
+ ],
117
+ )
118
+
119
+ dictionary_name = "Stamp#{next_stamp_dictionary_id}"
120
+
121
+ stamp_dictionary_registry[name] = {
122
+ stamp_dictionary_name: dictionary_name,
123
+ stamp_dictionary: dictionary,
124
+ }
125
+ dictionary
126
+ end
127
+
128
+ # Referencing annotations from a stamp XObject doesn't result
129
+ # in a working link. Instead, the references must be appended
130
+ # to the /Annot dictionary of the object that contains the
131
+ # call to the stamp object.
132
+ def update_annotation_references(annots)
133
+ if annots&.any?
134
+ state.page.dictionary.data[:Annots] ||= []
135
+ state.page.dictionary.data[:Annots] |= annots
136
+ end
137
+ end
138
+
139
+ def freeze_stamp_graphics
140
+ update_colors
141
+ write_line_width
142
+ write_stroke_cap_style
143
+ write_stroke_join_style
144
+ write_stroke_dash
145
+ end
146
+ end
147
+ end