hexapdf 0.19.2 → 0.20.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/data/hexapdf/cert/demo_cert.rb +22 -0
- data/data/hexapdf/cert/root-ca.crt +119 -0
- data/data/hexapdf/cert/signing.crt +125 -0
- data/data/hexapdf/cert/signing.key +52 -0
- data/data/hexapdf/cert/sub-ca.crt +125 -0
- data/data/hexapdf/encoding/glyphlist.txt +4283 -4282
- data/data/hexapdf/encoding/zapfdingbats.txt +203 -202
- data/lib/hexapdf/cli/info.rb +21 -1
- data/lib/hexapdf/configuration.rb +26 -0
- data/lib/hexapdf/content/processor.rb +1 -1
- data/lib/hexapdf/document/signatures.rb +327 -0
- data/lib/hexapdf/document.rb +26 -0
- data/lib/hexapdf/font/encoding/glyph_list.rb +5 -6
- data/lib/hexapdf/importer.rb +1 -1
- data/lib/hexapdf/object.rb +5 -3
- data/lib/hexapdf/parser.rb +14 -9
- data/lib/hexapdf/rectangle.rb +0 -6
- data/lib/hexapdf/revision.rb +13 -6
- data/lib/hexapdf/task/dereference.rb +12 -4
- data/lib/hexapdf/task/optimize.rb +3 -3
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
- data/lib/hexapdf/type/acro_form/field.rb +2 -0
- data/lib/hexapdf/type/acro_form/form.rb +9 -1
- data/lib/hexapdf/type/annotation.rb +36 -3
- data/lib/hexapdf/type/font_simple.rb +1 -1
- data/lib/hexapdf/type/object_stream.rb +3 -1
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +121 -0
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +95 -0
- data/lib/hexapdf/type/signature/handler.rb +140 -0
- data/lib/hexapdf/type/signature/verification_result.rb +92 -0
- data/lib/hexapdf/type/signature.rb +236 -0
- data/lib/hexapdf/type.rb +1 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +16 -8
- data/test/hexapdf/content/test_processor.rb +1 -1
- data/test/hexapdf/document/test_signatures.rb +225 -0
- data/test/hexapdf/task/test_optimize.rb +4 -1
- data/test/hexapdf/test_document.rb +28 -0
- data/test/hexapdf/test_object.rb +7 -2
- data/test/hexapdf/test_parser.rb +12 -0
- data/test/hexapdf/test_rectangle.rb +0 -7
- data/test/hexapdf/test_revision.rb +44 -14
- data/test/hexapdf/test_writer.rb +4 -3
- data/test/hexapdf/type/acro_form/test_field.rb +11 -1
- data/test/hexapdf/type/acro_form/test_form.rb +5 -0
- data/test/hexapdf/type/signature/common.rb +71 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
- data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
- data/test/hexapdf/type/signature/test_handler.rb +102 -0
- data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
- data/test/hexapdf/type/test_annotation.rb +40 -2
- data/test/hexapdf/type/test_font_simple.rb +5 -5
- data/test/hexapdf/type/test_object_stream.rb +9 -0
- data/test/hexapdf/type/test_signature.rb +131 -0
- metadata +21 -3
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
|
2
|
+
#
|
|
3
|
+
#--
|
|
4
|
+
# This file is part of HexaPDF.
|
|
5
|
+
#
|
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
+
# Copyright (C) 2014-2021 Thomas Leitner
|
|
8
|
+
#
|
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
16
|
+
#
|
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
20
|
+
# License for more details.
|
|
21
|
+
#
|
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
|
24
|
+
#
|
|
25
|
+
# The interactive user interfaces in modified source and object code
|
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
|
28
|
+
#
|
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
|
31
|
+
# is created or manipulated using HexaPDF.
|
|
32
|
+
#
|
|
33
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
|
+
#++
|
|
36
|
+
|
|
37
|
+
require 'openssl'
|
|
38
|
+
require 'hexapdf/error'
|
|
39
|
+
|
|
40
|
+
module HexaPDF
|
|
41
|
+
class Document
|
|
42
|
+
|
|
43
|
+
# This class provides methods for interacting with digital signatures of a PDF file.
|
|
44
|
+
class Signatures
|
|
45
|
+
|
|
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
|
+
#
|
|
49
|
+
# Additional functionality:
|
|
50
|
+
#
|
|
51
|
+
# * Optionally setting the reason, location and contact information.
|
|
52
|
+
# * Making the signature a certification signature by applying the DocMDP transform method.
|
|
53
|
+
#
|
|
54
|
+
# == Implementing a Signing Handler
|
|
55
|
+
#
|
|
56
|
+
# 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.
|
|
59
|
+
#
|
|
60
|
+
# Once a custom signing handler has been created, it can be registered under the
|
|
61
|
+
# 'signature.signing_handler' configuration option for easy use. It has to take keyword
|
|
62
|
+
# arguments in its initialize method to be compatible with the Signatures#handler method.
|
|
63
|
+
class DefaultHandler
|
|
64
|
+
|
|
65
|
+
# The certificate with which to sign the PDF.
|
|
66
|
+
attr_accessor :certificate
|
|
67
|
+
|
|
68
|
+
# The private key for the #certificate.
|
|
69
|
+
attr_accessor :key
|
|
70
|
+
|
|
71
|
+
# The certificate chain that should be embedded in the PDF; normally contains all
|
|
72
|
+
# certificates up to the root certificate.
|
|
73
|
+
attr_accessor :certificate_chain
|
|
74
|
+
|
|
75
|
+
# The reason for signing. If used, will be set on the signature object.
|
|
76
|
+
attr_accessor :reason
|
|
77
|
+
|
|
78
|
+
# The signing location. If used, will be set on the signature object.
|
|
79
|
+
attr_accessor :location
|
|
80
|
+
|
|
81
|
+
# The contact information. If used, will be set on the signature object.
|
|
82
|
+
attr_accessor :contact_info
|
|
83
|
+
|
|
84
|
+
# The DocMDP permissions that should be set on the document.
|
|
85
|
+
#
|
|
86
|
+
# See #doc_mdp_permissions=
|
|
87
|
+
attr_reader :doc_mdp_permissions
|
|
88
|
+
|
|
89
|
+
# Creates a new DefaultHandler with the given attributes.
|
|
90
|
+
def initialize(**arguments)
|
|
91
|
+
arguments.each {|name, value| send("#{name}=", value) }
|
|
92
|
+
end
|
|
93
|
+
|
|
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
|
+
# Sets the DocMDP permissions that should be applied to the document.
|
|
105
|
+
#
|
|
106
|
+
# Valid values for +permissions+ are:
|
|
107
|
+
#
|
|
108
|
+
# +nil+::
|
|
109
|
+
# Don't set any DocMDP permissions (default).
|
|
110
|
+
#
|
|
111
|
+
# +:no_changes+ or 1::
|
|
112
|
+
# No changes whatsoever are allowed.
|
|
113
|
+
#
|
|
114
|
+
# +:form_filling+ or 2::
|
|
115
|
+
# Only filling in forms and signing are allowed.
|
|
116
|
+
#
|
|
117
|
+
# +:form_filling_and_annotations+ or 3::
|
|
118
|
+
# Only filling in forms, signing and annotation creation/deletion/modification are
|
|
119
|
+
# allowed.
|
|
120
|
+
def doc_mdp_permissions=(permissions)
|
|
121
|
+
case permissions
|
|
122
|
+
when :no_changes, 1 then @doc_mdp_permissions = 1
|
|
123
|
+
when :form_filling, 2 then @doc_mdp_permissions = 2
|
|
124
|
+
when :form_filling_and_annotations, 3 then @doc_mdp_permissions = 3
|
|
125
|
+
when nil then @doc_mdp_permissions = nil
|
|
126
|
+
else
|
|
127
|
+
raise ArgumentError, "Invalid permissions value '#{permissions.inspect}'"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns the size of the signature that would be created.
|
|
132
|
+
def signature_size
|
|
133
|
+
sign("").size
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Finalizes the signature field as well as the signature dictionary before writing.
|
|
137
|
+
def finalize_objects(_signature_field, signature)
|
|
138
|
+
signature[:Reason] = reason if reason
|
|
139
|
+
signature[:Location] = location if location
|
|
140
|
+
signature[:ContactInfo] = contact_info if contact_info
|
|
141
|
+
|
|
142
|
+
if doc_mdp_permissions
|
|
143
|
+
doc = signature.document
|
|
144
|
+
if doc.signatures.count > 1
|
|
145
|
+
raise HexaPDF::Error, "Can set DocMDP access permissions only on first signature"
|
|
146
|
+
end
|
|
147
|
+
params = doc.add({Type: :TransformParams, V: :'1.2', P: doc_mdp_permissions})
|
|
148
|
+
sigref = doc.add({Type: :SigRef, TransformMethod: :DocMDP, DigestMethod: :SHA1,
|
|
149
|
+
TransformParams: params})
|
|
150
|
+
signature[:Reference] = [sigref]
|
|
151
|
+
(doc.catalog[:Perms] ||= {})[:DocMDP] = signature
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# 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
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
include Enumerable
|
|
165
|
+
|
|
166
|
+
# Creates a new Signatures object for the given PDF document.
|
|
167
|
+
def initialize(document)
|
|
168
|
+
@document = document
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Creates a signing handler with the given options and returns it.
|
|
172
|
+
#
|
|
173
|
+
# A signing handler name is mapped to a class via the 'signature.signing_handler'
|
|
174
|
+
# configuration option.
|
|
175
|
+
def handler(name: :default, **options)
|
|
176
|
+
handler = @document.config.constantize('signature.signing_handler', name) do
|
|
177
|
+
raise HexaPDF::Error, "No signing handler named '#{name}' is available"
|
|
178
|
+
end
|
|
179
|
+
handler.new(**options)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Adds a signature to the document and returns the corresponding signature object.
|
|
183
|
+
#
|
|
184
|
+
# This method will add a new signature to the document and write the updated document to the
|
|
185
|
+
# given file or IO stream. Afterwards the document can't be modified anymore and still retain
|
|
186
|
+
# a correct digital signature; create a new document based on the file or IO stream instead.
|
|
187
|
+
#
|
|
188
|
+
# +signature+::
|
|
189
|
+
# Can either be a signature object (determined via the /Type key), a signature field or
|
|
190
|
+
# +nil+. Providing a signature object or signature field provides for more control, e.g.:
|
|
191
|
+
#
|
|
192
|
+
# * Setting values for optional signature object fields like /Reason and /Location.
|
|
193
|
+
# * (In)directly specifying which signature field should be used.
|
|
194
|
+
#
|
|
195
|
+
# If a signature object is provided and it is not associated with an AcroForm signature
|
|
196
|
+
# field, a new signature field is created and added to the main AcroForm object, creating
|
|
197
|
+
# that if necessary.
|
|
198
|
+
#
|
|
199
|
+
# If a signature field is provided and it already has a signature object as field value,
|
|
200
|
+
# that signature object is discarded.
|
|
201
|
+
#
|
|
202
|
+
# If the signature field doesn't have a widget, a non-visible one is created on the first
|
|
203
|
+
# page.
|
|
204
|
+
#
|
|
205
|
+
# +handler+::
|
|
206
|
+
# The signing handler that provides the necessary methods for signing and adjusting the
|
|
207
|
+
# signature and signature field objects to one's liking, see #handler and DefaultHandler.
|
|
208
|
+
#
|
|
209
|
+
# +write_options+::
|
|
210
|
+
# The key-value pairs of this hash will be passed on to the HexaPDF::Document#write
|
|
211
|
+
# command. Note that +incremental+ will be automatically set if signing an already
|
|
212
|
+
# existing file.
|
|
213
|
+
def add(file_or_io, handler, signature: nil, write_options: {})
|
|
214
|
+
if signature && signature.type != :Sig
|
|
215
|
+
signature_field = signature
|
|
216
|
+
signature = signature_field.field_value
|
|
217
|
+
end
|
|
218
|
+
signature ||= @document.add({Type: :Sig})
|
|
219
|
+
|
|
220
|
+
# Prepare AcroForm
|
|
221
|
+
form = @document.acro_form(create: true)
|
|
222
|
+
form.signature_flag(:signatures_exist, :append_only)
|
|
223
|
+
|
|
224
|
+
# Prepare signature field
|
|
225
|
+
signature_field ||= form.each_field.find {|field| field.field_value == signature } ||
|
|
226
|
+
form.create_signature_field(generate_field_name)
|
|
227
|
+
signature_field.field_value = signature
|
|
228
|
+
|
|
229
|
+
if signature_field.each_widget.to_a.empty?
|
|
230
|
+
signature_field.create_widget(@document.pages[0], Rect: [0, 0, 0, 0])
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Prepare signature object
|
|
234
|
+
signature[:Filter] = handler.filter_name
|
|
235
|
+
signature[:SubFilter] = handler.sub_filter_name
|
|
236
|
+
signature[:ByteRange] = [0, 1_000_000_000_000, 1_000_000_000_000, 1_000_000_000_000]
|
|
237
|
+
signature[:Contents] = '00' * handler.signature_size # twice the size due to hex encoding
|
|
238
|
+
signature[:M] = Time.now
|
|
239
|
+
|
|
240
|
+
io = if file_or_io.kind_of?(String)
|
|
241
|
+
File.open(file_or_io, 'w+')
|
|
242
|
+
else
|
|
243
|
+
file_or_io
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Save the current state so that we can determine the correct /ByteRange value and set the
|
|
247
|
+
# values
|
|
248
|
+
handler.finalize_objects(signature_field, signature)
|
|
249
|
+
section = @document.write(io, incremental: true, **write_options)
|
|
250
|
+
data = section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort
|
|
251
|
+
index = data.index {|_pos, oid| oid == signature.oid }
|
|
252
|
+
signature_offset = data[index][0]
|
|
253
|
+
signature_length = data[index + 1][0] - data[index][0]
|
|
254
|
+
io.pos = signature_offset
|
|
255
|
+
signature_data = io.read(signature_length)
|
|
256
|
+
|
|
257
|
+
io.rewind
|
|
258
|
+
file_data = io.read
|
|
259
|
+
|
|
260
|
+
# Calculate the offsets for the /ByteRange
|
|
261
|
+
contents_offset = signature_offset + signature_data.index('Contents(') + 8
|
|
262
|
+
offset2 = contents_offset + signature[:Contents].size + 2 # +2 because of the needed < and >
|
|
263
|
+
length2 = file_data.size - offset2
|
|
264
|
+
signature[:ByteRange] = [0, contents_offset, offset2, length2]
|
|
265
|
+
|
|
266
|
+
# Set the correct /ByteRange value
|
|
267
|
+
signature_data.sub!(/ByteRange\[0 1000000000000 1000000000000 1000000000000\]/) do |match|
|
|
268
|
+
length = match.size
|
|
269
|
+
result = "ByteRange[0 #{contents_offset} #{offset2} #{length2}]"
|
|
270
|
+
result.ljust(length)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Now everything besides the /Contents value is correct, so we can read the contents for
|
|
274
|
+
# signing
|
|
275
|
+
file_data[signature_offset, signature_length] = signature_data
|
|
276
|
+
signed_contents = file_data[0, contents_offset] << file_data[offset2, length2]
|
|
277
|
+
signature[:Contents] = handler.sign(signed_contents)
|
|
278
|
+
|
|
279
|
+
# Set the correct /Contents value as hexstring
|
|
280
|
+
signature_data.sub!(/Contents\(0+\)/) do |match|
|
|
281
|
+
length = match.size
|
|
282
|
+
result = "Contents<#{signature[:Contents].unpack1('H*')}"
|
|
283
|
+
"#{result.ljust(length - 1, '0')}>"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
io.pos = signature_offset
|
|
287
|
+
io.write(signature_data)
|
|
288
|
+
|
|
289
|
+
signature
|
|
290
|
+
ensure
|
|
291
|
+
io.close if io && io != file_or_io
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# :call-seq:
|
|
295
|
+
# signatures.each {|signature| block } -> signatures
|
|
296
|
+
# signatures.each -> Enumerator
|
|
297
|
+
#
|
|
298
|
+
# Iterates over all signatures in the order they are found.
|
|
299
|
+
def each
|
|
300
|
+
return to_enum(__method__) unless block_given?
|
|
301
|
+
|
|
302
|
+
return [] unless (form = @document.acro_form)
|
|
303
|
+
form.each_field do |field|
|
|
304
|
+
yield(field.field_value) if field.field_type == :Sig && field.field_value
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Returns the number of signatures in the PDF document. May be zero if the document has no
|
|
309
|
+
# signatures.
|
|
310
|
+
def count
|
|
311
|
+
each.to_a.size
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
private
|
|
315
|
+
|
|
316
|
+
# Generates a field name for a signature field.
|
|
317
|
+
def generate_field_name
|
|
318
|
+
index = (@document.acro_form.each_field.
|
|
319
|
+
map {|field| field.full_field_name.scan(/\ASignature(\d+)/).first&.first.to_i }.
|
|
320
|
+
max || 0) + 1
|
|
321
|
+
"Signature#{index}"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
end
|
|
327
|
+
end
|
data/lib/hexapdf/document.rb
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
require 'stringio'
|
|
38
38
|
require 'hexapdf/error'
|
|
39
|
+
require 'hexapdf/data_dir'
|
|
39
40
|
require 'hexapdf/content'
|
|
40
41
|
require 'hexapdf/configuration'
|
|
41
42
|
require 'hexapdf/reference'
|
|
@@ -104,6 +105,7 @@ module HexaPDF
|
|
|
104
105
|
autoload(:Fonts, 'hexapdf/document/fonts')
|
|
105
106
|
autoload(:Images, 'hexapdf/document/images')
|
|
106
107
|
autoload(:Files, 'hexapdf/document/files')
|
|
108
|
+
autoload(:Signatures, 'hexapdf/document/signatures')
|
|
107
109
|
|
|
108
110
|
# :call-seq:
|
|
109
111
|
# Document.open(filename, **docargs) -> doc
|
|
@@ -617,6 +619,30 @@ module HexaPDF
|
|
|
617
619
|
@security_handler
|
|
618
620
|
end
|
|
619
621
|
|
|
622
|
+
# Returns +true+ if the document is signed, i.e. contains digital signatures.
|
|
623
|
+
def signed?
|
|
624
|
+
acro_form&.signature_flag?(:signatures_exist)
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# Returns an array with the digital signatures of this document.
|
|
628
|
+
def signatures
|
|
629
|
+
@signatures ||= Signatures.new(self)
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Signs the document and writes it to the given file or IO object.
|
|
633
|
+
#
|
|
634
|
+
# For details on the arguments +file_or_io+, +signature+ and +write_options+ see
|
|
635
|
+
# HexaPDF::Document::Signatures#add.
|
|
636
|
+
#
|
|
637
|
+
# The signing handler to be used is determined by the +handler+ argument together with the rest
|
|
638
|
+
# of the keyword arguments (see HexaPDF::Document::Signatures#handler for details).
|
|
639
|
+
#
|
|
640
|
+
# If not changed, the default signing handler is HexaPDF::Document::Signatures::DefaultHandler.
|
|
641
|
+
def sign(file_or_io, handler: :default, signature: nil, write_options: {}, **handler_options)
|
|
642
|
+
handler = signatures.handler(name: handler, **handler_options)
|
|
643
|
+
signatures.add(file_or_io, handler, signature: signature, write_options: write_options)
|
|
644
|
+
end
|
|
645
|
+
|
|
620
646
|
# Validates all objects, or, if +only_loaded+ is +true+, only loaded objects, with optional
|
|
621
647
|
# auto-correction, and returns +true+ if everything is fine.
|
|
622
648
|
#
|
|
@@ -131,13 +131,12 @@ module HexaPDF
|
|
|
131
131
|
def load_file(file)
|
|
132
132
|
name2uni = {}
|
|
133
133
|
uni2name = {}
|
|
134
|
-
File.open(file, '
|
|
134
|
+
File.open(file, 'r:UTF-8') do |f|
|
|
135
|
+
25.times { f.gets } # Skip comments
|
|
135
136
|
while (line = f.gets)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
name =
|
|
139
|
-
codes = line[index + 1, 50].split(" ").map(&:hex).pack('U*')
|
|
140
|
-
name2uni[name] = codes
|
|
137
|
+
name, codes = line.split(';', 2)
|
|
138
|
+
name = name.to_sym
|
|
139
|
+
name2uni[name] = codes.chomp!
|
|
141
140
|
uni2name[codes] = name unless uni2name.key?(codes)
|
|
142
141
|
end
|
|
143
142
|
end
|
data/lib/hexapdf/importer.rb
CHANGED
|
@@ -93,7 +93,7 @@ module HexaPDF
|
|
|
93
93
|
mapped_object = @mapper[object.data]&.__getobj__ if object.kind_of?(HexaPDF::Object)
|
|
94
94
|
if object.kind_of?(HexaPDF::Object) && object.document? && @source != object.document
|
|
95
95
|
raise HexaPDF::Error, "Import error: Incorrect document object for importer"
|
|
96
|
-
elsif mapped_object && mapped_object
|
|
96
|
+
elsif mapped_object && !mapped_object.null?
|
|
97
97
|
mapped_object
|
|
98
98
|
else
|
|
99
99
|
duplicate(object)
|
data/lib/hexapdf/object.rb
CHANGED
|
@@ -333,10 +333,12 @@ module HexaPDF
|
|
|
333
333
|
(oid == other.oid ? gen <=> other.gen : oid <=> other.oid)
|
|
334
334
|
end
|
|
335
335
|
|
|
336
|
-
# Returns +true+ if the other object is an Object and wraps the same #data structure,
|
|
337
|
-
# other object is a Reference with the same oid/gen
|
|
336
|
+
# Returns +true+ if the other object is an Object and wraps the same #data structure, if the
|
|
337
|
+
# other object is a Reference with the same oid/gen, or if this object is not indirect and its
|
|
338
|
+
# value is equal to the other object.
|
|
338
339
|
def ==(other)
|
|
339
|
-
(other.kind_of?(Object) && data == other.data) || (other.kind_of?(Reference) && other == self)
|
|
340
|
+
(other.kind_of?(Object) && data == other.data) || (other.kind_of?(Reference) && other == self) ||
|
|
341
|
+
(!indirect? && other == data.value)
|
|
340
342
|
end
|
|
341
343
|
|
|
342
344
|
# Returns +true+ if the other object references the same PDF object as this object.
|
data/lib/hexapdf/parser.rb
CHANGED
|
@@ -342,7 +342,8 @@ module HexaPDF
|
|
|
342
342
|
step_size = 1024
|
|
343
343
|
pos = @io.pos
|
|
344
344
|
eof_not_found = pos == 0
|
|
345
|
-
startxref_missing = false
|
|
345
|
+
startxref_missing = startxref_mangled = false
|
|
346
|
+
startxref_offset = nil
|
|
346
347
|
|
|
347
348
|
while pos != 0
|
|
348
349
|
@io.pos = [pos - step_size, 0].max
|
|
@@ -350,27 +351,31 @@ module HexaPDF
|
|
|
350
351
|
lines = @io.read(step_size + 40).split(/[\r\n]+/)
|
|
351
352
|
|
|
352
353
|
eof_index = lines.rindex {|l| l.strip == '%%EOF' }
|
|
353
|
-
|
|
354
|
+
if !eof_index
|
|
354
355
|
eof_not_found = true
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
356
|
+
elsif lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
|
|
357
|
+
startxref_offset = $1.to_i
|
|
358
|
+
startxref_mangled = true
|
|
359
|
+
break # we found it even if it the syntax is not entirely correct
|
|
360
|
+
elsif eof_index < 2 || lines[eof_index - 2].strip != "startxref"
|
|
358
361
|
startxref_missing = true
|
|
359
|
-
|
|
362
|
+
else
|
|
363
|
+
startxref_offset = lines[eof_index - 1].to_i
|
|
364
|
+
break # we found it
|
|
360
365
|
end
|
|
361
|
-
|
|
362
|
-
break # we found the startxref offset
|
|
363
366
|
end
|
|
364
367
|
|
|
365
368
|
if eof_not_found
|
|
366
369
|
maybe_raise("PDF file trailer with end-of-file marker not found", pos: pos,
|
|
367
370
|
force: !eof_index)
|
|
371
|
+
elsif startxref_mangled
|
|
372
|
+
maybe_raise("PDF file trailer keyword startxref on same line as value", pos: pos)
|
|
368
373
|
elsif startxref_missing
|
|
369
374
|
maybe_raise("PDF file trailer is missing startxref keyword", pos: pos,
|
|
370
375
|
force: eof_index < 2 || lines[eof_index - 2].strip != "startxref")
|
|
371
376
|
end
|
|
372
377
|
|
|
373
|
-
@startxref_offset =
|
|
378
|
+
@startxref_offset = startxref_offset
|
|
374
379
|
end
|
|
375
380
|
|
|
376
381
|
# Returns the reconstructed revision.
|
data/lib/hexapdf/rectangle.rb
CHANGED
|
@@ -114,12 +114,6 @@ module HexaPDF
|
|
|
114
114
|
self[3] = self[1] + val
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
# Compares this rectangle to +other+ like in Object#== but also allows comparison to simple
|
|
118
|
-
# arrays if the rectangle is a direct object.
|
|
119
|
-
def ==(other)
|
|
120
|
-
super || (other.kind_of?(Array) && !indirect? && other == data.value)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
117
|
private
|
|
124
118
|
|
|
125
119
|
# Ensures that the value is an array containing four numbers that specify the bottom left and
|
data/lib/hexapdf/revision.rb
CHANGED
|
@@ -190,7 +190,7 @@ module HexaPDF
|
|
|
190
190
|
obj.data.value = nil
|
|
191
191
|
obj.document = nil
|
|
192
192
|
if mark_as_free
|
|
193
|
-
add_without_check(HexaPDF::Object.new(
|
|
193
|
+
add_without_check(HexaPDF::Object.new(obj.data))
|
|
194
194
|
else
|
|
195
195
|
@xref_section.delete(ref_or_oid)
|
|
196
196
|
@objects.delete(ref_or_oid)
|
|
@@ -228,7 +228,8 @@ module HexaPDF
|
|
|
228
228
|
# revision.each_modified_object {|obj| block } -> revision
|
|
229
229
|
# revision.each_modified_object -> Enumerator
|
|
230
230
|
#
|
|
231
|
-
# Calls the given block once for each object that has been modified since it was loaded.
|
|
231
|
+
# Calls the given block once for each object that has been modified since it was loaded. Deleted
|
|
232
|
+
# object and cross-reference streams are ignored.
|
|
232
233
|
#
|
|
233
234
|
# Note that this also means that for revisions without an associated cross-reference section all
|
|
234
235
|
# loaded objects will be yielded.
|
|
@@ -238,12 +239,18 @@ module HexaPDF
|
|
|
238
239
|
@objects.each do |oid, gen, obj|
|
|
239
240
|
if @xref_section.entry?(oid, gen)
|
|
240
241
|
stored_obj = @loader.call(@xref_section[oid, gen])
|
|
241
|
-
if
|
|
242
|
-
|
|
242
|
+
next if (stored_obj.type == :ObjStm || stored_obj.type == :XRef) && obj.null?
|
|
243
|
+
|
|
244
|
+
streams_are_same = (obj.data.stream == stored_obj.data.stream)
|
|
245
|
+
next if obj.value == stored_obj.value && streams_are_same
|
|
246
|
+
|
|
247
|
+
if obj.value.kind_of?(Hash) && stored_obj.value.kind_of?(Hash)
|
|
248
|
+
keys = obj.value.keys | stored_obj.value.keys
|
|
249
|
+
next if keys.all? {|key| obj[key] == stored_obj[key] } && streams_are_same
|
|
243
250
|
end
|
|
244
|
-
else
|
|
245
|
-
yield(obj)
|
|
246
251
|
end
|
|
252
|
+
|
|
253
|
+
yield(obj)
|
|
247
254
|
end
|
|
248
255
|
|
|
249
256
|
self
|
|
@@ -68,9 +68,9 @@ module HexaPDF
|
|
|
68
68
|
|
|
69
69
|
def execute #:nodoc:
|
|
70
70
|
if @object
|
|
71
|
-
|
|
71
|
+
dereference_all(@object)
|
|
72
72
|
else
|
|
73
|
-
|
|
73
|
+
dereference_all(@doc.trailer)
|
|
74
74
|
@result = []
|
|
75
75
|
@doc.each(only_current: false) do |obj|
|
|
76
76
|
if !@seen.key?(obj.data) && obj.type != :ObjStm && obj.type != :XRef
|
|
@@ -83,6 +83,11 @@ module HexaPDF
|
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
def dereference_all(object) # :nodoc:
|
|
87
|
+
@dereference_later = [object]
|
|
88
|
+
dereference(@dereference_later.pop) until @dereference_later.empty?
|
|
89
|
+
end
|
|
90
|
+
|
|
86
91
|
def dereference(object) #:nodoc:
|
|
87
92
|
return object if object.nil? || @seen.key?(object.data)
|
|
88
93
|
@seen[object.data] = true
|
|
@@ -97,9 +102,12 @@ module HexaPDF
|
|
|
97
102
|
when Array
|
|
98
103
|
val.map! {|v| recurse(v) }
|
|
99
104
|
when HexaPDF::Reference
|
|
100
|
-
|
|
105
|
+
val = @doc.object(val)
|
|
106
|
+
@dereference_later.push(val)
|
|
107
|
+
val
|
|
101
108
|
when HexaPDF::Object
|
|
102
|
-
|
|
109
|
+
@dereference_later.push(val)
|
|
110
|
+
val
|
|
103
111
|
else
|
|
104
112
|
val
|
|
105
113
|
end
|
|
@@ -234,7 +234,7 @@ module HexaPDF
|
|
|
234
234
|
page.contents = processor.result
|
|
235
235
|
page[:Contents].set_filter(:FlateDecode)
|
|
236
236
|
xobjects = page.resources[:XObject]
|
|
237
|
-
processor.used_references.each {|ref| used_refs[xobjects[ref]] = true }
|
|
237
|
+
processor.used_references.each {|ref| used_refs[xobjects[ref]] = true } if xobjects
|
|
238
238
|
end
|
|
239
239
|
used_refs
|
|
240
240
|
end
|
|
@@ -245,7 +245,7 @@ module HexaPDF
|
|
|
245
245
|
unless used_refs
|
|
246
246
|
used_refs = {}
|
|
247
247
|
doc.pages.each do |page|
|
|
248
|
-
xobjects = page.resources[:XObject]
|
|
248
|
+
next unless (xobjects = page.resources[:XObject])
|
|
249
249
|
HexaPDF::Content::Parser.parse(page.contents) do |op, operands|
|
|
250
250
|
used_refs[xobjects[operands[0]]] = true if op == :Do
|
|
251
251
|
end
|
|
@@ -253,7 +253,7 @@ module HexaPDF
|
|
|
253
253
|
end
|
|
254
254
|
|
|
255
255
|
doc.pages.each do |page|
|
|
256
|
-
xobjects = page.resources[:XObject]
|
|
256
|
+
next unless (xobjects = page.resources[:XObject])
|
|
257
257
|
xobjects.each do |key, obj|
|
|
258
258
|
next if used_refs[obj]
|
|
259
259
|
xobjects.delete(key)
|
|
@@ -458,10 +458,8 @@ module HexaPDF
|
|
|
458
458
|
option_items = @field.option_items
|
|
459
459
|
top_index = @field.list_box_top_index
|
|
460
460
|
items = [Layout::TextFragment.create(option_items[top_index..-1].join("\n"), style)]
|
|
461
|
-
|
|
462
|
-
indices = @field
|
|
463
|
-
value_indices = [@field.field_value].flatten.compact.map {|val| option_items.index(val) }
|
|
464
|
-
indices = value_indices if indices != value_indices
|
|
461
|
+
# Should use /I but if it differs from /V, we need to use /V; so just use /V...
|
|
462
|
+
indices = [@field.field_value].flatten.compact.map {|val| option_items.index(val) }
|
|
465
463
|
|
|
466
464
|
layouter = Layout::TextLayouter.new(style)
|
|
467
465
|
layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
|
|
@@ -75,7 +75,7 @@ module HexaPDF
|
|
|
75
75
|
|
|
76
76
|
define_type :XXAcroForm
|
|
77
77
|
|
|
78
|
-
define_field :Fields, type: PDFArray, required: true, version: '1.2'
|
|
78
|
+
define_field :Fields, type: PDFArray, required: true, default: [], version: '1.2'
|
|
79
79
|
define_field :NeedAppearances, type: Boolean, default: false
|
|
80
80
|
define_field :SigFlags, type: Integer, version: '1.3'
|
|
81
81
|
define_field :CO, type: PDFArray, version: '1.3'
|
|
@@ -323,6 +323,14 @@ module HexaPDF
|
|
|
323
323
|
end
|
|
324
324
|
end
|
|
325
325
|
|
|
326
|
+
# Creates a signature field with the given name and adds it to the form.
|
|
327
|
+
#
|
|
328
|
+
# The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
|
|
329
|
+
# fields must already exist. If it doesn't contain dots, a top-level field is created.
|
|
330
|
+
def create_signature_field(name)
|
|
331
|
+
create_field(name, :Sig) {}
|
|
332
|
+
end
|
|
333
|
+
|
|
326
334
|
# Returns the dictionary containing the default resources for form field appearance streams.
|
|
327
335
|
def default_resources
|
|
328
336
|
self[:DR] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
|