hexapdf 0.26.2 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
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