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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/data/hexapdf/cert/demo_cert.rb +22 -0
  4. data/data/hexapdf/cert/root-ca.crt +119 -0
  5. data/data/hexapdf/cert/signing.crt +125 -0
  6. data/data/hexapdf/cert/signing.key +52 -0
  7. data/data/hexapdf/cert/sub-ca.crt +125 -0
  8. data/data/hexapdf/encoding/glyphlist.txt +4283 -4282
  9. data/data/hexapdf/encoding/zapfdingbats.txt +203 -202
  10. data/lib/hexapdf/cli/info.rb +21 -1
  11. data/lib/hexapdf/configuration.rb +26 -0
  12. data/lib/hexapdf/content/processor.rb +1 -1
  13. data/lib/hexapdf/document/signatures.rb +327 -0
  14. data/lib/hexapdf/document.rb +26 -0
  15. data/lib/hexapdf/font/encoding/glyph_list.rb +5 -6
  16. data/lib/hexapdf/importer.rb +1 -1
  17. data/lib/hexapdf/object.rb +5 -3
  18. data/lib/hexapdf/parser.rb +14 -9
  19. data/lib/hexapdf/rectangle.rb +0 -6
  20. data/lib/hexapdf/revision.rb +13 -6
  21. data/lib/hexapdf/task/dereference.rb +12 -4
  22. data/lib/hexapdf/task/optimize.rb +3 -3
  23. data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
  24. data/lib/hexapdf/type/acro_form/field.rb +2 -0
  25. data/lib/hexapdf/type/acro_form/form.rb +9 -1
  26. data/lib/hexapdf/type/annotation.rb +36 -3
  27. data/lib/hexapdf/type/font_simple.rb +1 -1
  28. data/lib/hexapdf/type/object_stream.rb +3 -1
  29. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +121 -0
  30. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +95 -0
  31. data/lib/hexapdf/type/signature/handler.rb +140 -0
  32. data/lib/hexapdf/type/signature/verification_result.rb +92 -0
  33. data/lib/hexapdf/type/signature.rb +236 -0
  34. data/lib/hexapdf/type.rb +1 -0
  35. data/lib/hexapdf/version.rb +1 -1
  36. data/lib/hexapdf/writer.rb +16 -8
  37. data/test/hexapdf/content/test_processor.rb +1 -1
  38. data/test/hexapdf/document/test_signatures.rb +225 -0
  39. data/test/hexapdf/task/test_optimize.rb +4 -1
  40. data/test/hexapdf/test_document.rb +28 -0
  41. data/test/hexapdf/test_object.rb +7 -2
  42. data/test/hexapdf/test_parser.rb +12 -0
  43. data/test/hexapdf/test_rectangle.rb +0 -7
  44. data/test/hexapdf/test_revision.rb +44 -14
  45. data/test/hexapdf/test_writer.rb +4 -3
  46. data/test/hexapdf/type/acro_form/test_field.rb +11 -1
  47. data/test/hexapdf/type/acro_form/test_form.rb +5 -0
  48. data/test/hexapdf/type/signature/common.rb +71 -0
  49. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
  50. data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
  51. data/test/hexapdf/type/signature/test_handler.rb +102 -0
  52. data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
  53. data/test/hexapdf/type/test_annotation.rb +40 -2
  54. data/test/hexapdf/type/test_font_simple.rb +5 -5
  55. data/test/hexapdf/type/test_object_stream.rb +9 -0
  56. data/test/hexapdf/type/test_signature.rb +131 -0
  57. 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
@@ -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, 'rb') do |f|
134
+ File.open(file, 'r:UTF-8') do |f|
135
+ 25.times { f.gets } # Skip comments
135
136
  while (line = f.gets)
136
- next if line.start_with?('#')
137
- index = line.index(';')
138
- name = line[0, index].to_sym
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
@@ -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 == @destination.object(mapped_object)
96
+ elsif mapped_object && !mapped_object.null?
97
97
  mapped_object
98
98
  else
99
99
  duplicate(object)
@@ -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, or if the
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.
@@ -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
- unless eof_index
354
+ if !eof_index
354
355
  eof_not_found = true
355
- next
356
- end
357
- unless eof_index >= 2 && lines[eof_index - 2].strip == "startxref"
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
- next
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 = lines[eof_index - 1].to_i
378
+ @startxref_offset = startxref_offset
374
379
  end
375
380
 
376
381
  # Returns the reconstructed revision.
@@ -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
@@ -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(nil, oid: obj.oid, gen: obj.gen))
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 obj.data.value != stored_obj.data.value || obj.data.stream != stored_obj.data.stream
242
- yield(obj)
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
- dereference(@object)
71
+ dereference_all(@object)
72
72
  else
73
- dereference(@doc.trailer)
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
- dereference(@doc.object(val))
105
+ val = @doc.object(val)
106
+ @dereference_later.push(val)
107
+ val
101
108
  when HexaPDF::Object
102
- dereference(val)
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[:I] || []
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)
@@ -310,6 +310,8 @@ module HexaPDF
310
310
  widget = document.wrap(self)
311
311
  end
312
312
 
313
+ widget.flag(:print)
314
+ widget[:P] = page
313
315
  (page[:Annots] ||= []) << widget
314
316
 
315
317
  widget
@@ -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]},