hexapdf 0.26.2 → 0.28.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +115 -1
- data/README.md +1 -1
- data/examples/013-text_layouter_shapes.rb +8 -8
- data/examples/016-frame_automatic_box_placement.rb +3 -3
- data/examples/017-frame_text_flow.rb +3 -3
- data/examples/019-acro_form.rb +14 -3
- data/examples/020-column_box.rb +3 -3
- data/examples/023-images.rb +30 -0
- data/lib/hexapdf/cli/info.rb +5 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/cli/split.rb +8 -8
- data/lib/hexapdf/cli/watermark.rb +2 -2
- data/lib/hexapdf/configuration.rb +3 -2
- data/lib/hexapdf/content/canvas.rb +8 -3
- data/lib/hexapdf/dictionary.rb +4 -17
- data/lib/hexapdf/document/destinations.rb +42 -5
- data/lib/hexapdf/document/signatures.rb +265 -48
- data/lib/hexapdf/document.rb +6 -10
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/importer.rb +35 -27
- data/lib/hexapdf/layout/list_box.rb +1 -5
- data/lib/hexapdf/object.rb +5 -0
- data/lib/hexapdf/parser.rb +14 -0
- data/lib/hexapdf/revision.rb +15 -12
- data/lib/hexapdf/revisions.rb +7 -1
- data/lib/hexapdf/tokenizer.rb +15 -9
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/field.rb +11 -5
- data/lib/hexapdf/type/acro_form/form.rb +61 -8
- data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -0
- data/lib/hexapdf/type/catalog.rb +1 -1
- data/lib/hexapdf/type/font_true_type.rb +14 -0
- data/lib/hexapdf/type/object_stream.rb +2 -2
- data/lib/hexapdf/type/outline.rb +19 -1
- data/lib/hexapdf/type/outline_item.rb +72 -14
- data/lib/hexapdf/type/page.rb +95 -64
- data/lib/hexapdf/type/resources.rb +13 -17
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +16 -2
- data/lib/hexapdf/type/signature.rb +10 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +5 -3
- data/test/hexapdf/content/test_canvas.rb +5 -0
- data/test/hexapdf/document/test_destinations.rb +41 -0
- data/test/hexapdf/document/test_pages.rb +2 -2
- data/test/hexapdf/document/test_signatures.rb +139 -19
- data/test/hexapdf/encryption/test_aes.rb +1 -1
- data/test/hexapdf/filter/test_predictor.rb +0 -1
- data/test/hexapdf/layout/test_box.rb +2 -1
- data/test/hexapdf/layout/test_column_box.rb +1 -1
- data/test/hexapdf/layout/test_list_box.rb +1 -1
- data/test/hexapdf/test_document.rb +2 -8
- data/test/hexapdf/test_importer.rb +27 -6
- data/test/hexapdf/test_parser.rb +19 -2
- data/test/hexapdf/test_revision.rb +15 -14
- data/test/hexapdf/test_revisions.rb +63 -12
- data/test/hexapdf/test_stream.rb +1 -1
- data/test/hexapdf/test_tokenizer.rb +10 -1
- data/test/hexapdf/test_writer.rb +11 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
- data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
- data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_field.rb +4 -4
- data/test/hexapdf/type/acro_form/test_form.rb +65 -0
- data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
- data/test/hexapdf/type/signature/common.rb +54 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +21 -0
- data/test/hexapdf/type/test_catalog.rb +5 -2
- data/test/hexapdf/type/test_font_true_type.rb +20 -0
- data/test/hexapdf/type/test_object_stream.rb +2 -1
- data/test/hexapdf/type/test_outline.rb +4 -1
- data/test/hexapdf/type/test_outline_item.rb +62 -1
- data/test/hexapdf/type/test_page.rb +103 -45
- data/test/hexapdf/type/test_page_tree_node.rb +4 -2
- data/test/hexapdf/type/test_resources.rb +0 -5
- data/test/hexapdf/type/test_signature.rb +8 -0
- data/test/test_helper.rb +1 -1
- 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
|
47
|
-
#
|
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
|
-
# #
|
58
|
-
#
|
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
|
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(
|
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
|
-
#
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
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, **
|
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(**
|
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
|
-
#
|
213
|
-
#
|
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] =
|
236
|
-
signature[:SubFilter] =
|
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
|
-
|
250
|
-
|
251
|
-
|
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.
|
260
|
-
|
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 =
|
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
|
-
|
278
|
-
|
279
|
-
signature[:Contents] = handler.sign(
|
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
|
|
data/lib/hexapdf/document.rb
CHANGED
@@ -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
|
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
|
257
|
+
# the previously imported object is returned.
|
259
258
|
#
|
260
259
|
# See: Importer
|
261
260
|
def import(obj)
|
262
|
-
|
263
|
-
|
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
|
data/lib/hexapdf/importer.rb
CHANGED
@@ -60,61 +60,69 @@ module HexaPDF
|
|
60
60
|
|
61
61
|
end
|
62
62
|
|
63
|
-
# Returns the Importer object for copying objects
|
64
|
-
|
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.
|
68
|
-
source = NullableWeakRef.new(source)
|
66
|
+
@map.keep_if {|_, v| v.destination.weakref_alive? }
|
69
67
|
destination = NullableWeakRef.new(destination)
|
70
|
-
@map[
|
68
|
+
@map[destination.hash] ||= new(destination)
|
71
69
|
end
|
72
70
|
|
73
71
|
private_class_method :new
|
74
72
|
|
75
|
-
attr_reader :
|
73
|
+
attr_reader :destination #:nodoc:
|
76
74
|
|
77
|
-
# Initializes a new importer that can import objects
|
78
|
-
|
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
|
-
|
86
|
-
|
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
|
-
#
|
92
|
-
|
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
|
95
|
-
|
96
|
-
|
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
|
-
|
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.
|
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])
|
data/lib/hexapdf/object.rb
CHANGED
@@ -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!
|
data/lib/hexapdf/parser.rb
CHANGED
@@ -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
|