hexapdf 0.26.2 → 0.28.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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -1
  3. data/README.md +1 -1
  4. data/examples/013-text_layouter_shapes.rb +8 -8
  5. data/examples/016-frame_automatic_box_placement.rb +3 -3
  6. data/examples/017-frame_text_flow.rb +3 -3
  7. data/examples/019-acro_form.rb +14 -3
  8. data/examples/020-column_box.rb +3 -3
  9. data/examples/023-images.rb +30 -0
  10. data/lib/hexapdf/cli/info.rb +5 -1
  11. data/lib/hexapdf/cli/inspect.rb +2 -2
  12. data/lib/hexapdf/cli/split.rb +8 -8
  13. data/lib/hexapdf/cli/watermark.rb +2 -2
  14. data/lib/hexapdf/configuration.rb +3 -2
  15. data/lib/hexapdf/content/canvas.rb +8 -3
  16. data/lib/hexapdf/dictionary.rb +4 -17
  17. data/lib/hexapdf/document/destinations.rb +42 -5
  18. data/lib/hexapdf/document/signatures.rb +265 -48
  19. data/lib/hexapdf/document.rb +6 -10
  20. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  21. data/lib/hexapdf/importer.rb +35 -27
  22. data/lib/hexapdf/layout/list_box.rb +1 -5
  23. data/lib/hexapdf/object.rb +5 -0
  24. data/lib/hexapdf/parser.rb +14 -0
  25. data/lib/hexapdf/revision.rb +15 -12
  26. data/lib/hexapdf/revisions.rb +7 -1
  27. data/lib/hexapdf/tokenizer.rb +15 -9
  28. data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
  29. data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
  30. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
  31. data/lib/hexapdf/type/acro_form/field.rb +11 -5
  32. data/lib/hexapdf/type/acro_form/form.rb +61 -8
  33. data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
  34. data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
  35. data/lib/hexapdf/type/annotations/widget.rb +3 -0
  36. data/lib/hexapdf/type/catalog.rb +1 -1
  37. data/lib/hexapdf/type/font_true_type.rb +14 -0
  38. data/lib/hexapdf/type/object_stream.rb +2 -2
  39. data/lib/hexapdf/type/outline.rb +19 -1
  40. data/lib/hexapdf/type/outline_item.rb +72 -14
  41. data/lib/hexapdf/type/page.rb +95 -64
  42. data/lib/hexapdf/type/resources.rb +13 -17
  43. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +16 -2
  44. data/lib/hexapdf/type/signature.rb +10 -0
  45. data/lib/hexapdf/version.rb +1 -1
  46. data/lib/hexapdf/writer.rb +5 -3
  47. data/test/hexapdf/content/test_canvas.rb +5 -0
  48. data/test/hexapdf/document/test_destinations.rb +41 -0
  49. data/test/hexapdf/document/test_pages.rb +2 -2
  50. data/test/hexapdf/document/test_signatures.rb +139 -19
  51. data/test/hexapdf/encryption/test_aes.rb +1 -1
  52. data/test/hexapdf/filter/test_predictor.rb +0 -1
  53. data/test/hexapdf/layout/test_box.rb +2 -1
  54. data/test/hexapdf/layout/test_column_box.rb +1 -1
  55. data/test/hexapdf/layout/test_list_box.rb +1 -1
  56. data/test/hexapdf/test_document.rb +2 -8
  57. data/test/hexapdf/test_importer.rb +27 -6
  58. data/test/hexapdf/test_parser.rb +19 -2
  59. data/test/hexapdf/test_revision.rb +15 -14
  60. data/test/hexapdf/test_revisions.rb +63 -12
  61. data/test/hexapdf/test_stream.rb +1 -1
  62. data/test/hexapdf/test_tokenizer.rb +10 -1
  63. data/test/hexapdf/test_writer.rb +11 -3
  64. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
  65. data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
  66. data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
  67. data/test/hexapdf/type/acro_form/test_field.rb +4 -4
  68. data/test/hexapdf/type/acro_form/test_form.rb +65 -0
  69. data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
  70. data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
  71. data/test/hexapdf/type/signature/common.rb +54 -0
  72. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +21 -0
  73. data/test/hexapdf/type/test_catalog.rb +5 -2
  74. data/test/hexapdf/type/test_font_true_type.rb +20 -0
  75. data/test/hexapdf/type/test_object_stream.rb +2 -1
  76. data/test/hexapdf/type/test_outline.rb +4 -1
  77. data/test/hexapdf/type/test_outline_item.rb +62 -1
  78. data/test/hexapdf/type/test_page.rb +103 -45
  79. data/test/hexapdf/type/test_page_tree_node.rb +4 -2
  80. data/test/hexapdf/type/test_resources.rb +0 -5
  81. data/test/hexapdf/type/test_signature.rb +8 -0
  82. data/test/test_helper.rb +1 -1
  83. metadata +61 -4
@@ -35,7 +35,9 @@
35
35
  #++
36
36
 
37
37
  require 'openssl'
38
+ require 'net/http'
38
39
  require 'hexapdf/error'
40
+ require 'stringio'
39
41
 
40
42
  module HexaPDF
41
43
  class Document
@@ -43,19 +45,59 @@ module HexaPDF
43
45
  # This class provides methods for interacting with digital signatures of a PDF file.
44
46
  class Signatures
45
47
 
46
- # This is the default signing handler which provides the ability to sign a document with a
47
- # provided certificate using the adb.pkcs7.detached algorithm.
48
+ # This is the default signing handler which provides the ability to sign a document with the
49
+ # adbe.pkcs7.detached or ETSI.CAdES.detached algorithms. It is registered under the :default
50
+ # name.
51
+ #
52
+ # == Usage
53
+ #
54
+ # The signing handler is used by default by all methods that need a signing handler. Therefore
55
+ # it is usually only necessary to provide the actual attribute values.
56
+ #
57
+ # This handler provides two ways to create the PKCS#7 signed-data structure required by
58
+ # Signatures#add:
59
+ #
60
+ # * By providing the signing certificate together with the signing key and the certificate
61
+ # chain. This way HexaPDF itself does the signing. It is the preferred way if all the needed
62
+ # information is available.
63
+ #
64
+ # Assign the respective data to the #certificate, #key and #certificate_chain attributes.
65
+ #
66
+ # * By using an external signing mechanism. Here the actual signing happens "outside" of
67
+ # HexaPDF, for example, in custom code or even asynchronously. This is needed in case the
68
+ # signing certificate plus key are not directly available but only an interface to them
69
+ # (e.g. when dealing with a HSM).
70
+ #
71
+ # Assign a callable object to #external_signing. If the signing process needs to be
72
+ # asynchronous, make sure to set the #signature_size appropriately, return an empty string
73
+ # during signing and later use Signatures.embed_signature to embed the actual signature.
48
74
  #
49
75
  # Additional functionality:
50
76
  #
51
77
  # * Optionally setting the reason, location and contact information.
52
78
  # * Making the signature a certification signature by applying the DocMDP transform method.
53
79
  #
80
+ # Example:
81
+ #
82
+ # # Signing using certificate + key
83
+ # document.sign("output.pdf", certificate: my_cert, key: my_key,
84
+ # certificate_chain: my_chain)
85
+ #
86
+ # # Signing using an external mechanism:
87
+ # signing_proc = lambda do |io, byte_range|
88
+ # io.pos = byte_range[0]
89
+ # data = io.read(byte_range[1])
90
+ # io.pos = byte_range[2]
91
+ # data << io.read(byte_range[3])
92
+ # signing_service.pkcs7_sign(data)
93
+ # end
94
+ # document.sign("output.pdf", signature_size: 10_000, external_signing: signing_proc)
95
+ #
54
96
  # == Implementing a Signing Handler
55
97
  #
56
98
  # This class also serves as an example on how to create a custom handler: The public methods
57
- # #filter_name, #sub_filter_name, #signature_size, #finalize_objects and #sign are used by the
58
- # digital signature algorithm.
99
+ # #signature_size, #finalize_objects and #sign are used by the digital signature algorithm.
100
+ # See their descriptions for details.
59
101
  #
60
102
  # Once a custom signing handler has been created, it can be registered under the
61
103
  # 'signature.signing_handler' configuration option for easy use. It has to take keyword
@@ -72,6 +114,13 @@ module HexaPDF
72
114
  # certificates up to the root certificate.
73
115
  attr_accessor :certificate_chain
74
116
 
117
+ # A callable object fulfilling the same role as the #sign method that is used instead of the
118
+ # default mechanism for signing.
119
+ #
120
+ # If this attribute is set, the attributes #certificate, #key and #certificate_chain are not
121
+ # used.
122
+ attr_accessor :external_signing
123
+
75
124
  # The reason for signing. If used, will be set on the signature object.
76
125
  attr_accessor :reason
77
126
 
@@ -81,6 +130,21 @@ module HexaPDF
81
130
  # The contact information. If used, will be set on the signature object.
82
131
  attr_accessor :contact_info
83
132
 
133
+ # The size of the serialized signature that should be reserved.
134
+ #
135
+ # If this attribute has not been set, an empty string will be signed using #sign to
136
+ # determine the signature size.
137
+ #
138
+ # The size needs to be at least as big as the final signature, otherwise signing results in
139
+ # an error.
140
+ attr_writer :signature_size
141
+
142
+ # The type of signature to be written (i.e. the value of the /SubFilter key).
143
+ #
144
+ # The value can either be :adobe (the default; uses a detached PKCS7 signature) or :etsi
145
+ # (uses an ETSI CAdES compatible signature).
146
+ attr_accessor :signature_type
147
+
84
148
  # The DocMDP permissions that should be set on the document.
85
149
  #
86
150
  # See #doc_mdp_permissions=
@@ -88,19 +152,10 @@ module HexaPDF
88
152
 
89
153
  # Creates a new DefaultHandler with the given attributes.
90
154
  def initialize(**arguments)
155
+ @signature_size = nil
91
156
  arguments.each {|name, value| send("#{name}=", value) }
92
157
  end
93
158
 
94
- # Returns the name to be set on the /Filter key when using this signing handler.
95
- def filter_name
96
- :'Adobe.PPKLite'
97
- end
98
-
99
- # Returns the name to be set on the /SubFilter key when using this signing handler.
100
- def sub_filter_name
101
- :'adbe.pkcs7.detached'
102
- end
103
-
104
159
  # Sets the DocMDP permissions that should be applied to the document.
105
160
  #
106
161
  # Valid values for +permissions+ are:
@@ -128,13 +183,17 @@ module HexaPDF
128
183
  end
129
184
  end
130
185
 
131
- # Returns the size of the signature that would be created.
186
+ # Returns the size of the serialized signature that should be reserved.
187
+ #
188
+ # If a custom size is set using #signature_size=, it used. Otherwise the size is determined
189
+ # by using #sign to sign an empty string.
132
190
  def signature_size
133
- sign("").size
191
+ @signature_size || sign(StringIO.new, [0, 0, 0, 0]).size
134
192
  end
135
193
 
136
194
  # Finalizes the signature field as well as the signature dictionary before writing.
137
195
  def finalize_objects(_signature_field, signature)
196
+ signature[:SubFilter] = :'ETSI.CAdES.detached' if signature_type == :etsi
138
197
  signature[:Reason] = reason if reason
139
198
  signature[:Location] = location if location
140
199
  signature[:ContactInfo] = contact_info if contact_info
@@ -153,14 +212,173 @@ module HexaPDF
153
212
  end
154
213
 
155
214
  # Returns the DER serialized OpenSSL::PKCS7 structure containing the signature for the given
156
- # data.
157
- def sign(data)
158
- OpenSSL::PKCS7.sign(@certificate, @key, data, @certificate_chain,
159
- OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
215
+ # IO byte ranges.
216
+ #
217
+ # The +byte_range+ argument is an array containing four numbers [offset1, length1, offset2,
218
+ # length2]. The offset numbers are byte positions in the +io+ argument and the to-be-signed
219
+ # data can be determined by reading length bytes at the offsets.
220
+ def sign(io, byte_range)
221
+ if external_signing
222
+ external_signing.call(io, byte_range)
223
+ else
224
+ io.pos = byte_range[0]
225
+ data = io.read(byte_range[1])
226
+ io.pos = byte_range[2]
227
+ data << io.read(byte_range[3])
228
+ OpenSSL::PKCS7.sign(@certificate, @key, data, @certificate_chain,
229
+ OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
230
+ end
231
+ end
232
+
233
+ end
234
+
235
+ # This is a signing handler for adding a timestamp signature (a PDF2.0 feature) to a PDF
236
+ # document. It is registered under the :timestamp name.
237
+ #
238
+ # The timestamp is provided by a timestamp authority and establishes the document contents at
239
+ # the time indicated in the timestamp. Timestamping a PDF document is usually done in context
240
+ # of long term validation but can also be done standalone.
241
+ #
242
+ # == Usage
243
+ #
244
+ # It is necessary to provide at least the URL of the timestamp authority server (TSA) via
245
+ # #tsa_url, everything else is optional and uses default values. The TSA server must not use
246
+ # authentication to be usable.
247
+ #
248
+ # Example:
249
+ #
250
+ # document.sign("output.pdf", handler: :timestamp, tsa_url: 'https://freetsa.org/tsr')
251
+ class TimestampHandler
252
+
253
+ # The URL of the timestamp authority server.
254
+ #
255
+ # This value is required.
256
+ attr_accessor :tsa_url
257
+
258
+ # The hash algorithm to use for timestamping. Defaults to SHA512.
259
+ attr_accessor :tsa_hash_algorithm
260
+
261
+ # The policy OID to use for timestamping. Defaults to +nil+.
262
+ attr_accessor :tsa_policy_id
263
+
264
+ # The size of the serialized signature that should be reserved.
265
+ #
266
+ # If this attribute has not been set, an empty string will be signed using #sign to
267
+ # determine the signature size which will contact the TSA server
268
+ #
269
+ # The size needs to be at least as big as the final signature, otherwise signing results in
270
+ # an error.
271
+ attr_writer :signature_size
272
+
273
+ # The reason for timestamping. If used, will be set on the signature object.
274
+ attr_accessor :reason
275
+
276
+ # The timestamping location. If used, will be set on the signature object.
277
+ attr_accessor :location
278
+
279
+ # The contact information. If used, will be set on the signature object.
280
+ attr_accessor :contact_info
281
+
282
+ # Creates a new TimestampHandler with the given attributes.
283
+ def initialize(**arguments)
284
+ @signature_size = nil
285
+ arguments.each {|name, value| send("#{name}=", value) }
286
+ end
287
+
288
+ # Returns the size of the serialized signature that should be reserved.
289
+ def signature_size
290
+ @signature_size || (sign(StringIO.new, [0, 0, 0, 0]).size * 1.5).to_i
291
+ end
292
+
293
+ # Finalizes the signature field as well as the signature dictionary before writing.
294
+ def finalize_objects(_signature_field, signature)
295
+ signature.document.version = '2.0'
296
+ signature[:Type] = :DocTimeStamp
297
+ signature[:SubFilter] = :'ETSI.RFC3161'
298
+ signature[:Reason] = reason if reason
299
+ signature[:Location] = location if location
300
+ signature[:ContactInfo] = contact_info if contact_info
301
+ end
302
+
303
+ # Returns the DER serialized OpenSSL::PKCS7 structure containing the timestamp token for the
304
+ # given IO byte ranges.
305
+ def sign(io, byte_range)
306
+ hash_algorithm = tsa_hash_algorithm || 'SHA512'
307
+ digest = OpenSSL::Digest.new(hash_algorithm)
308
+ io.pos = byte_range[0]
309
+ digest << io.read(byte_range[1])
310
+ io.pos = byte_range[2]
311
+ digest << io.read(byte_range[3])
312
+
313
+ req = OpenSSL::Timestamp::Request.new
314
+ req.algorithm = hash_algorithm
315
+ req.message_imprint = digest.digest
316
+ req.policy_id = tsa_policy_id if tsa_policy_id
317
+
318
+ http_response = Net::HTTP.post(URI(tsa_url), req.to_der,
319
+ 'content-type' => 'application/timestamp-query')
320
+ if http_response.kind_of?(Net::HTTPOK)
321
+ response = OpenSSL::Timestamp::Response.new(http_response.body)
322
+ if response.status == 0
323
+ response.token.to_der
324
+ else
325
+ raise HexaPDF::Error, "Timestamp token could not be created: #{response.failure_info}"
326
+ end
327
+ else
328
+ raise HexaPDF::Error, "Invalid TSA server response: #{http_response.body}"
329
+ end
160
330
  end
161
331
 
162
332
  end
163
333
 
334
+ # Embeds the given +signature+ into the /Contents value of the newest signature dictionary of
335
+ # the PDF document given by the +io+ argument.
336
+ #
337
+ # This functionality can be used together with the support for external signing (see
338
+ # DefaultHandler and DefaultHandler#external_signing) to implement asynchronous signing.
339
+ #
340
+ # Note: This will, most probably, only work on documents prepared for external signing by
341
+ # HexaPDF and not by other libraries.
342
+ def self.embed_signature(io, signature)
343
+ doc = HexaPDF::Document.new(io: io)
344
+ signature_dict = doc.signatures.find {|sig| doc.revisions.current.object(sig) == sig }
345
+ signature_dict_offset, signature_dict_length = locate_signature_dict(
346
+ doc.revisions.current.xref_section,
347
+ doc.revisions.parser.startxref_offset,
348
+ signature_dict.oid
349
+ )
350
+ io.pos = signature_dict_offset
351
+ signature_data = io.read(signature_dict_length)
352
+ replace_signature_contents(signature_data, signature)
353
+ io.pos = signature_dict_offset
354
+ io.write(signature_data)
355
+ end
356
+
357
+ # Uses the information in the given cross-reference section as well as the byte offset of the
358
+ # cross-reference section to calculate the offset and length of the signature dictionary with
359
+ # the given object id.
360
+ def self.locate_signature_dict(xref_section, start_xref_position, signature_oid)
361
+ data = xref_section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort <<
362
+ [start_xref_position, nil]
363
+ index = data.index {|_pos, oid| oid == signature_oid }
364
+ [data[index][0], data[index + 1][0] - data[index][0]]
365
+ end
366
+
367
+ # Replaces the value of the /Contents key in the serialized +signature_data+ with the value of
368
+ # +contents+.
369
+ def self.replace_signature_contents(signature_data, contents)
370
+ signature_data.sub!(/Contents(?:\(.*?\)|<.*?>)/) do |match|
371
+ length = match.size
372
+ result = "Contents<#{contents.unpack1('H*')}"
373
+ if length < result.size
374
+ raise HexaPDF::Error, "The reserved space for the signature was too small " \
375
+ "(#{(length - 10) / 2} vs #{(result.size - 10) / 2}) - use the handlers " \
376
+ "#signature_size method to increase the reserved space"
377
+ end
378
+ "#{result.ljust(length - 1, '0')}>"
379
+ end
380
+ end
381
+
164
382
  include Enumerable
165
383
 
166
384
  # Creates a new Signatures object for the given PDF document.
@@ -168,15 +386,15 @@ module HexaPDF
168
386
  @document = document
169
387
  end
170
388
 
171
- # Creates a signing handler with the given options and returns it.
389
+ # Creates a signing handler with the given attributes and returns it.
172
390
  #
173
391
  # A signing handler name is mapped to a class via the 'signature.signing_handler'
174
392
  # configuration option. The default signing handler is DefaultHandler.
175
- def handler(name: :default, **options)
393
+ def handler(name: :default, **attributes)
176
394
  handler = @document.config.constantize('signature.signing_handler', name) do
177
395
  raise HexaPDF::Error, "No signing handler named '#{name}' is available"
178
396
  end
179
- handler.new(**options)
397
+ handler.new(**attributes)
180
398
  end
181
399
 
182
400
  # Adds a signature to the document and returns the corresponding signature object.
@@ -209,8 +427,15 @@ module HexaPDF
209
427
  #
210
428
  # +write_options+::
211
429
  # The key-value pairs of this hash will be passed on to the HexaPDF::Document#write
212
- # command. Note that +incremental+ will be automatically set if signing an already
213
- # existing file.
430
+ # method. Note that +incremental+ will be automatically set to ensure proper behaviour.
431
+ #
432
+ # The used signature object will have the following default values set:
433
+ #
434
+ # /Filter:: /Adobe.PPKLite
435
+ # /SubFilter:: /adbe.pkcs7.detached
436
+ # /M:: The current time.
437
+ #
438
+ # These values can be overridden in the #finalize_objects method of the signature handler.
214
439
  def add(file_or_io, handler, signature: nil, write_options: {})
215
440
  if signature && signature.type != :Sig
216
441
  signature_field = signature
@@ -232,11 +457,12 @@ module HexaPDF
232
457
  end
233
458
 
234
459
  # Prepare signature object
235
- signature[:Filter] = handler.filter_name
236
- signature[:SubFilter] = handler.sub_filter_name
460
+ signature[:Filter] = :'Adobe.PPKLite'
461
+ signature[:SubFilter] = :'adbe.pkcs7.detached'
462
+ signature[:M] = Time.now
463
+ handler.finalize_objects(signature_field, signature)
237
464
  signature[:ByteRange] = [0, 1_000_000_000_000, 1_000_000_000_000, 1_000_000_000_000]
238
465
  signature[:Contents] = '00' * handler.signature_size # twice the size due to hex encoding
239
- signature[:M] = Time.now
240
466
 
241
467
  io = if file_or_io.kind_of?(String)
242
468
  File.open(file_or_io, 'wb+')
@@ -246,23 +472,19 @@ module HexaPDF
246
472
 
247
473
  # Save the current state so that we can determine the correct /ByteRange value and set the
248
474
  # values
249
- handler.finalize_objects(signature_field, signature)
250
- start_xref_position, section = @document.write(io, incremental: true, **write_options)
251
- data = section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort <<
252
- [start_xref_position, nil]
253
- index = data.index {|_pos, oid| oid == signature.oid }
254
- signature_offset = data[index][0]
255
- signature_length = data[index + 1][0] - data[index][0]
475
+ start_xref, section = @document.write(io, incremental: true, **write_options)
476
+ signature_offset, signature_length = self.class.locate_signature_dict(section, start_xref,
477
+ signature.oid)
256
478
  io.pos = signature_offset
257
479
  signature_data = io.read(signature_length)
258
480
 
259
- io.rewind
260
- file_data = io.read
481
+ io.seek(0, IO::SEEK_END)
482
+ file_size = io.pos
261
483
 
262
484
  # Calculate the offsets for the /ByteRange
263
485
  contents_offset = signature_offset + signature_data.index('Contents(') + 8
264
486
  offset2 = contents_offset + signature[:Contents].size + 2 # +2 because of the needed < and >
265
- length2 = file_data.size - offset2
487
+ length2 = file_size - offset2
266
488
  signature[:ByteRange] = [0, contents_offset, offset2, length2]
267
489
 
268
490
  # Set the correct /ByteRange value
@@ -274,17 +496,12 @@ module HexaPDF
274
496
 
275
497
  # Now everything besides the /Contents value is correct, so we can read the contents for
276
498
  # signing
277
- file_data[signature_offset, signature_length] = signature_data
278
- signed_contents = file_data[0, contents_offset] << file_data[offset2, length2]
279
- signature[:Contents] = handler.sign(signed_contents)
280
-
281
- # Set the correct /Contents value as hexstring
282
- signature_data.sub!(/Contents\(0+\)/) do |match|
283
- length = match.size
284
- result = "Contents<#{signature[:Contents].unpack1('H*')}"
285
- "#{result.ljust(length - 1, '0')}>"
286
- end
499
+ io.pos = signature_offset
500
+ io.write(signature_data)
501
+ signature[:Contents] = handler.sign(io, signature[:ByteRange].value)
287
502
 
503
+ # And now replace the /Contents value
504
+ self.class.replace_signature_contents(signature_data, signature[:Contents])
288
505
  io.pos = signature_offset
289
506
  io.write(signature_data)
290
507
 
@@ -164,6 +164,8 @@ module HexaPDF
164
164
  def initialize(io: nil, decryption_opts: {}, config: {})
165
165
  @config = Configuration.with_defaults(config)
166
166
  @version = '1.2'
167
+ @cache = Hash.new {|h, k| h[k] = {} }
168
+ @listeners = {}
167
169
 
168
170
  @revisions = Revisions.from_io(self, io)
169
171
  @security_handler = if encrypted? && @config['document.auto_decrypt']
@@ -171,9 +173,6 @@ module HexaPDF
171
173
  else
172
174
  nil
173
175
  end
174
-
175
- @listeners = {}
176
- @cache = Hash.new {|h, k| h[k] = {} }
177
176
  end
178
177
 
179
178
  # :call-seq:
@@ -251,19 +250,16 @@ module HexaPDF
251
250
  # :call-seq:
252
251
  # doc.import(obj) -> imported_object
253
252
  #
254
- # Imports the given, with a different document associated PDF object and returns the imported
253
+ # Imports the given object from a different HexaPDF::Document instance and returns the imported
255
254
  # object.
256
255
  #
257
256
  # If the same argument is provided in multiple invocations, the import is done only once and
258
- # the previously imoprted object is returned.
257
+ # the previously imported object is returned.
259
258
  #
260
259
  # See: Importer
261
260
  def import(obj)
262
- if !obj.kind_of?(HexaPDF::Object) || !obj.document? || obj.document == self
263
- raise ArgumentError, "Importing only works for PDF objects associated " \
264
- "with another document"
265
- end
266
- HexaPDF::Importer.for(source: obj.document, destination: self).import(obj)
261
+ source = (obj.kind_of?(HexaPDF::Object) ? obj.document : nil)
262
+ HexaPDF::Importer.for(self).import(obj, source: source)
267
263
  end
268
264
 
269
265
  # Wraps the given object inside a HexaPDF::Object class which allows one to use
@@ -49,7 +49,7 @@ module HexaPDF
49
49
  module ASCII85Decode
50
50
 
51
51
  VALUE_TO_CHAR = {} #:nodoc:
52
- (0..84).each do |i|
52
+ 85.times do |i|
53
53
  VALUE_TO_CHAR[i] = (i + 33).chr
54
54
  end
55
55
 
@@ -60,61 +60,69 @@ module HexaPDF
60
60
 
61
61
  end
62
62
 
63
- # Returns the Importer object for copying objects from the +source+ to the +destination+
64
- # document.
65
- def self.for(source:, destination:)
63
+ # Returns the Importer object for copying objects to the +destination+ document.
64
+ def self.for(destination)
66
65
  @map ||= {}
67
- @map.keep_if {|_, v| v.source.weakref_alive? && v.destination.weakref_alive? }
68
- source = NullableWeakRef.new(source)
66
+ @map.keep_if {|_, v| v.destination.weakref_alive? }
69
67
  destination = NullableWeakRef.new(destination)
70
- @map[[source.hash, destination.hash]] ||= new(source: source, destination: destination)
68
+ @map[destination.hash] ||= new(destination)
71
69
  end
72
70
 
73
71
  private_class_method :new
74
72
 
75
- attr_reader :source, :destination #:nodoc:
73
+ attr_reader :destination #:nodoc:
76
74
 
77
- # Initializes a new importer that can import objects from the +source+ document to the
78
- # +destination+ document.
79
- def initialize(source:, destination:)
80
- @source = source
75
+ # Initializes a new importer that can import objects to the +destination+ document.
76
+ def initialize(destination)
81
77
  @destination = destination
82
78
  @mapper = {}
83
79
  end
84
80
 
85
- # Imports the given +object+ from the source to the destination object and returns the
86
- # imported object.
81
+ SourceWrapper = Struct.new(:source) #:nodoc:
82
+
83
+ # Imports the given +object+ to the destination object and returns the imported object.
87
84
  #
88
85
  # Note: Indirect objects are automatically added to the destination document but direct or
89
86
  # simple objects are not.
90
87
  #
91
- # An error is raised if the object doesn't belong to the +source+ document.
92
- def import(object)
88
+ # The +source+ argument should be +nil+ or set to the source document of the imported object. If
89
+ # it is +nil+, the source document is dynamically identified. If this identification is not
90
+ # possible and the source document would be needed, an error is raised.
91
+ def import(object, source: nil)
92
+ internal_import(object, SourceWrapper.new(source))
93
+ end
94
+
95
+ private
96
+
97
+ # Does the actual importing of the given +object+, using +wrapper+ to store/use the source
98
+ # document.
99
+ def internal_import(object, wrapper)
93
100
  mapped_object = @mapper[object.data]&.__getobj__ if object.kind_of?(HexaPDF::Object)
94
- if object.kind_of?(HexaPDF::Object) && object.document? && @source != object.document
95
- raise HexaPDF::Error, "Import error: Incorrect document object for importer"
96
- elsif mapped_object && !mapped_object.null?
101
+ if mapped_object && !mapped_object.null?
102
+ if object.class != mapped_object.class
103
+ mapped_object = @destination.wrap(mapped_object, type: object.class)
104
+ end
97
105
  mapped_object
98
106
  else
99
- duplicate(object)
107
+ duplicate(object, wrapper)
100
108
  end
101
109
  end
102
110
 
103
- private
104
-
105
111
  # Recursively duplicates the object.
106
112
  #
107
113
  # PDF objects are automatically added to the destination document if they are indirect objects
108
114
  # in the source document.
109
- def duplicate(object)
115
+ def duplicate(object, wrapper)
110
116
  case object
111
117
  when Hash
112
- object.transform_values {|v| duplicate(v) }
118
+ object.transform_values {|v| duplicate(v, wrapper) }
113
119
  when Array
114
- object.map {|v| duplicate(v) }
120
+ object.map {|v| duplicate(v, wrapper) }
115
121
  when HexaPDF::Reference
116
- import(@source.object(object))
122
+ raise HexaPDF::Error, "Import error: No source document specified" unless wrapper.source
123
+ internal_import(wrapper.source.object(object), wrapper)
117
124
  when HexaPDF::Object
125
+ wrapper.source ||= object.document
118
126
  if object.type == :Catalog || object.type == :Pages
119
127
  @mapper[object.data] = nil
120
128
  elsif (mapped_object = @mapper[object.data]&.__getobj__) && !mapped_object.null?
@@ -129,8 +137,8 @@ module HexaPDF
129
137
  @destination.add(obj) if object.indirect?
130
138
 
131
139
  obj.data.stream = obj.data.stream.dup if obj.data.stream.kind_of?(String)
132
- obj.data.value = duplicate(obj.data.value)
133
- obj.data.value.update(duplicate(object.copy_inherited_values)) if object.type == :Page
140
+ obj.data.value = duplicate(obj.data.value, wrapper)
141
+ obj.data.value.update(duplicate(object.copy_inherited_values, wrapper)) if object.type == :Page
134
142
  obj
135
143
  end
136
144
  when String
@@ -207,7 +207,7 @@ module HexaPDF
207
207
  @results = []
208
208
  @results_item_marker_x = []
209
209
 
210
- @children.each_with_index do |child, index|
210
+ @children.each do |child|
211
211
  shape = Geom2D::Polygon([left, top - height],
212
212
  [left + width, top - height],
213
213
  [left + width, top],
@@ -217,11 +217,7 @@ module HexaPDF
217
217
  remove_indent_from_frame_shape(shape) unless shape.polygons.empty?
218
218
  end
219
219
 
220
- #p [:list, left, width, shape]
221
-
222
220
  item_frame = Frame.new(item_frame_left, top - height, item_frame_width, height, shape: shape)
223
-
224
- #p [index, item_frame.x, @results_item_marker_x]
225
221
  @results_item_marker_x << item_frame.x - content_indentation
226
222
 
227
223
  box_fitter = BoxFitter.new([item_frame])
@@ -159,6 +159,11 @@ module HexaPDF
159
159
  object
160
160
  end
161
161
 
162
+ # Returns +nil+ to end the recursion for field searching in Dictionary.field.
163
+ def self.field(_name)
164
+ nil
165
+ end
166
+
162
167
  # The wrapped HexaPDF::PDFData value.
163
168
  #
164
169
  # This attribute is not part of the public API!
@@ -70,6 +70,19 @@ module HexaPDF
70
70
  !@reconstructed_revision.nil?
71
71
  end
72
72
 
73
+ # Returns +true+ if the PDF file is a linearized file.
74
+ def linearized?
75
+ @linearized ||=
76
+ begin
77
+ @tokenizer.pos = @header_offset
78
+ 3.times { @tokenizer.next_token } # parse: oid gen obj
79
+ obj = @tokenizer.next_object
80
+ obj.kind_of?(Hash) && obj.key?(:Linearized)
81
+ rescue MalformedPDFError
82
+ false
83
+ end
84
+ end
85
+
73
86
  # Loads the indirect (potentially compressed) object specified by the given cross-reference
74
87
  # entry.
75
88
  #
@@ -137,6 +150,7 @@ module HexaPDF
137
150
  @tokenizer.pos -= 6
138
151
  else
139
152
  maybe_raise("Invalid value after '#{oid} #{gen} obj', treating as null", pos: @tokenizer.pos)
153
+ return [nil, oid, gen, nil]
140
154
  end
141
155
  end
142
156
  end